integrar la 1.3.5 del dev y perfiles Minimo, Acampada y Activista
Lo del dev, tal cual desde el nodejs-project de su APK oficial 1.3.5, mezclado a tres bandas con la 1.3.3 como base. Lo que no habiamos tocado se coge entero. Conflictos: - backend.js: su middleware nuevo de media va delante de nuestro router. - main_views.js y bookmark_view.js: lo suyo. Su menu de tarjeta (⋯) ya aparta borrar del pulgar, que era lo que hacia nuestro cambio. - settings_view.js y tribes_view.js: lo suyo mas nuestra linea de enganche. La 1.3.5 trae una cabecera de modulo para el movil (apkModbar) que se lleva la primera caja de fichas, el buscador y los contadores. Con el skin Activista su CSS (OasisMobile.css) no se carga: se le da estilo en mobile_ux.css, los avisos suben encima con order y se ajusta la cabecera nueva de las tarjetas. Filtrar va ahora dentro de la caja de fichas (casilla + etiqueta) para viajar con ella. Perfiles: Minimo (6 modulos, de serie), Acampada (16), Activista (24, con tribus, votaciones, campañas, encuestas y tareas) y despues los del dev por numero de modulos.
This commit is contained in:
parent
ff91ef938c
commit
83f26ba19e
157 changed files with 6633 additions and 4710 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"description": "Oasis - Social Networking Utopia",
|
"description": "Oasis - Social Networking Utopia",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -230,7 +230,141 @@ const stripPdfMetadata = (buffer) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STRIP_CHUNK = 64 * 1024;
|
||||||
|
const STRIP_IN_MEMORY_MAX = 256 * 1024 * 1024;
|
||||||
|
const FFMPEG_STRIP_MS = 120000;
|
||||||
|
const MP4_LIKE = /mp4|quicktime|m4a|m4v|3gpp/;
|
||||||
|
const MP4_METADATA_BOXES = new Set(['udta', 'meta']);
|
||||||
|
const MP4_WALKED_BOXES = new Set(['moov', 'trak']);
|
||||||
|
const XMP_UUID = Buffer.from('be7acfcb97a942e89c71999491e3afac', 'hex');
|
||||||
|
const WAV_METADATA_CHUNKS = new Set(['LIST', 'id3 ', 'ID3 ', 'bext', 'iXML', '_PMX', 'XMP ']);
|
||||||
|
const FLAC_METADATA_BLOCKS = new Set([4, 6]);
|
||||||
|
const FLAC_PADDING = 1;
|
||||||
|
const ID3V1_NO_GENRE = 255;
|
||||||
|
const FFMPEG_FORMATS = { 'video/webm': 'webm', 'audio/webm': 'webm', 'video/x-matroska': 'matroska', 'audio/x-matroska': 'matroska', 'video/ogg': 'ogg', 'audio/ogg': 'ogg', 'audio/opus': 'ogg', 'video/x-msvideo': 'avi' };
|
||||||
|
|
||||||
|
const readAt = async (fh, pos, len) => {
|
||||||
|
const buf = Buffer.alloc(len);
|
||||||
|
const { bytesRead } = await fh.read(buf, 0, len, pos);
|
||||||
|
return buf.subarray(0, bytesRead);
|
||||||
|
};
|
||||||
|
|
||||||
|
const zeroAt = async (fh, pos, len) => {
|
||||||
|
const zeros = Buffer.alloc(Math.min(len, STRIP_CHUNK));
|
||||||
|
for (let done = 0; done < len; done += zeros.length) await fh.write(zeros, 0, Math.min(zeros.length, len - done), pos + done);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripMp4 = async (fh, start, end, depth = 0) => {
|
||||||
|
let pos = start;
|
||||||
|
while (pos + 8 <= end) {
|
||||||
|
const head = await readAt(fh, pos, 16);
|
||||||
|
if (head.length < 8) return;
|
||||||
|
let size = head.readUInt32BE(0);
|
||||||
|
let header = 8;
|
||||||
|
const type = head.toString('latin1', 4, 8);
|
||||||
|
if (size === 1) { if (head.length < 16) return; size = Number(head.readBigUInt64BE(8)); header = 16; }
|
||||||
|
else if (size === 0) size = end - pos;
|
||||||
|
if (size < header || pos + size > end) return;
|
||||||
|
const isXmp = type === 'uuid' && (await readAt(fh, pos + header, XMP_UUID.length)).equals(XMP_UUID);
|
||||||
|
if (MP4_METADATA_BOXES.has(type) || isXmp) {
|
||||||
|
await fh.write(Buffer.from('free', 'latin1'), 0, 4, pos + 4);
|
||||||
|
await zeroAt(fh, pos + header, size - header);
|
||||||
|
}
|
||||||
|
else if (MP4_WALKED_BOXES.has(type) && depth < MP4_WALKED_BOXES.size) await stripMp4(fh, pos + header, pos + size, depth + 1);
|
||||||
|
pos += size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripMp3 = async (fh, size) => {
|
||||||
|
const head = await readAt(fh, 0, 10);
|
||||||
|
if (head.length === 10 && head.toString('latin1', 0, 3) === 'ID3') {
|
||||||
|
const tagSize = ((head[6] & 0x7f) << 21) | ((head[7] & 0x7f) << 14) | ((head[8] & 0x7f) << 7) | (head[9] & 0x7f);
|
||||||
|
const footer = head[5] & 0x10 ? 10 : 0;
|
||||||
|
await fh.write(Buffer.from([0]), 0, 1, 5);
|
||||||
|
await zeroAt(fh, 10, Math.min(tagSize + footer, size - 10));
|
||||||
|
}
|
||||||
|
if (size >= 128 && (await readAt(fh, size - 128, 3)).toString('latin1') === 'TAG') {
|
||||||
|
await zeroAt(fh, size - 125, 125);
|
||||||
|
await fh.write(Buffer.from([ID3V1_NO_GENRE]), 0, 1, size - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripFlac = async (fh, size) => {
|
||||||
|
if ((await readAt(fh, 0, 4)).toString('latin1') !== 'fLaC') return;
|
||||||
|
for (let pos = 4; pos + 4 <= size;) {
|
||||||
|
const head = await readAt(fh, pos, 4);
|
||||||
|
if (head.length < 4) return;
|
||||||
|
const last = head[0] & 0x80;
|
||||||
|
const len = head.readUIntBE(1, 3);
|
||||||
|
if (FLAC_METADATA_BLOCKS.has(head[0] & 0x7f)) {
|
||||||
|
await fh.write(Buffer.from([last | FLAC_PADDING]), 0, 1, pos);
|
||||||
|
await zeroAt(fh, pos + 4, len);
|
||||||
|
}
|
||||||
|
if (last) return;
|
||||||
|
pos += 4 + len;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripWav = async (fh, size) => {
|
||||||
|
const head = await readAt(fh, 0, 12);
|
||||||
|
if (head.length < 12 || head.toString('latin1', 0, 4) !== 'RIFF' || head.toString('latin1', 8, 12) !== 'WAVE') return;
|
||||||
|
for (let pos = 12; pos + 8 <= size;) {
|
||||||
|
const chunk = await readAt(fh, pos, 8);
|
||||||
|
if (chunk.length < 8) return;
|
||||||
|
const len = chunk.readUInt32LE(4);
|
||||||
|
if (WAV_METADATA_CHUNKS.has(chunk.toString('latin1', 0, 4))) {
|
||||||
|
await fh.write(Buffer.from('JUNK', 'latin1'), 0, 4, pos);
|
||||||
|
await zeroAt(fh, pos + 8, Math.min(len, size - pos - 8));
|
||||||
|
}
|
||||||
|
pos += 8 + len + (len & 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripWithFfmpeg = (filepath, format) => new Promise((resolve) => {
|
||||||
|
const out = `${filepath}.clean`;
|
||||||
|
const args = ['-v', 'error', '-y', '-i', filepath, '-map', '0', '-map_metadata', '-1', '-c', 'copy', '-f', format, out];
|
||||||
|
require('child_process').execFile(process.env.OASIS_FFMPEG || 'ffmpeg', args, { timeout: FFMPEG_STRIP_MS }, async (err) => {
|
||||||
|
try {
|
||||||
|
if (err) await promisesFs.unlink(out).catch(() => {});
|
||||||
|
else await promisesFs.rename(out, filepath);
|
||||||
|
} catch (_) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const stripFileMetadata = async (filepath, mime) => {
|
||||||
|
const m = String(mime || '');
|
||||||
|
try {
|
||||||
|
const { size } = await promisesFs.stat(filepath);
|
||||||
|
if ((m.startsWith('image/') && m !== 'image/gif') || m === 'application/pdf') {
|
||||||
|
if (size > STRIP_IN_MEMORY_MAX) return;
|
||||||
|
const data = await promisesFs.readFile(filepath);
|
||||||
|
const clean = m === 'application/pdf' ? stripPdfMetadata(data) : await stripImageMetadata(data);
|
||||||
|
if (clean !== data) await promisesFs.writeFile(filepath, clean);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m.startsWith('audio/') && !m.startsWith('video/')) return;
|
||||||
|
if (FFMPEG_FORMATS[m]) { await stripWithFfmpeg(filepath, FFMPEG_FORMATS[m]); return; }
|
||||||
|
const fh = await promisesFs.open(filepath, 'r+');
|
||||||
|
try {
|
||||||
|
if (MP4_LIKE.test(m)) await stripMp4(fh, 0, size);
|
||||||
|
else if (m === 'audio/mpeg') await stripMp3(fh, size);
|
||||||
|
else if (m === 'audio/flac' || m === 'audio/x-flac') await stripFlac(fh, size);
|
||||||
|
else if (/wav|wave/.test(m)) await stripWav(fh, size);
|
||||||
|
} finally { await fh.close(); }
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_BLOB_SIZE = 75 * 1024 * 1024;
|
const MAX_BLOB_SIZE = 75 * 1024 * 1024;
|
||||||
|
const KIND_MIME = { image: /^image\//, audio: /^audio\//, video: /^video\//, document: /^application\/pdf$/, torrent: /^application\/x-bittorrent$/ };
|
||||||
|
const looksLikeTorrent = (data) => data.length > 0 && data[0] === 0x64 && data.subarray(0, 4096).includes(Buffer.from('4:info'));
|
||||||
|
|
||||||
|
class WrongFileTypeError extends Error {
|
||||||
|
constructor(fileName, mime) {
|
||||||
|
super(`Wrong file type: ${fileName} (${mime})`);
|
||||||
|
this.name = 'WrongFileTypeError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FileTooLargeError extends Error {
|
class FileTooLargeError extends Error {
|
||||||
constructor(fileName, fileSize) {
|
constructor(fileName, fileSize) {
|
||||||
|
|
@ -253,7 +387,7 @@ const oggMime = (buffer) => {
|
||||||
|
|
||||||
const sealedRef = (sealed, mime, name) => `/blob/sealed/${encodeURIComponent(sealed.manifestBlobId)}?k=${encodeURIComponent(sealed.key)}&t=${encodeURIComponent(mime)}&n=${encodeURIComponent(name)}`;
|
const sealedRef = (sealed, mime, name) => `/blob/sealed/${encodeURIComponent(sealed.manifestBlobId)}?k=${encodeURIComponent(sealed.key)}&t=${encodeURIComponent(mime)}&n=${encodeURIComponent(name)}`;
|
||||||
|
|
||||||
const storeUploadedFile = async function (blobUpload, seal = null) {
|
const storeUploadedFile = async function (blobUpload, seal = null, kind = null) {
|
||||||
if (!blobUpload || !blobUpload.filepath) return null;
|
if (!blobUpload || !blobUpload.filepath) return null;
|
||||||
|
|
||||||
let data = await promisesFs.readFile(blobUpload.filepath);
|
let data = await promisesFs.readFile(blobUpload.filepath);
|
||||||
|
|
@ -294,11 +428,16 @@ const storeUploadedFile = async function (blobUpload, seal = null) {
|
||||||
if (!blob.mime) {
|
if (!blob.mime) {
|
||||||
blob.mime = 'application/octet-stream';
|
blob.mime = 'application/octet-stream';
|
||||||
}
|
}
|
||||||
|
if (blob.mime === 'application/octet-stream' && looksLikeTorrent(data)) blob.mime = 'application/x-bittorrent';
|
||||||
|
if (kind && KIND_MIME[kind] && !KIND_MIME[kind].test(blob.mime)) throw new WrongFileTypeError(blob.name, blob.mime);
|
||||||
|
|
||||||
if (blob.mime.startsWith('image/') && blob.mime !== 'image/gif') {
|
if (blob.mime.startsWith('image/') && blob.mime !== 'image/gif') {
|
||||||
data = await stripImageMetadata(data);
|
data = await stripImageMetadata(data);
|
||||||
} else if (blob.mime === 'application/pdf') {
|
} else if (blob.mime === 'application/pdf') {
|
||||||
data = stripPdfMetadata(data);
|
data = stripPdfMetadata(data);
|
||||||
|
} else if (blob.mime.startsWith('audio/') || blob.mime.startsWith('video/')) {
|
||||||
|
await stripFileMetadata(blobUpload.filepath, blob.mime);
|
||||||
|
data = await promisesFs.readFile(blobUpload.filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof seal === 'function') {
|
if (typeof seal === 'function') {
|
||||||
|
|
@ -324,11 +463,11 @@ const storeUploadedFile = async function (blobUpload, seal = null) {
|
||||||
return `\n[${blob.name}](${blob.id})`;
|
return `\n[${blob.name}](${blob.id})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlobUpload = async function (ctx, fileFieldName, seal = null) {
|
const handleBlobUpload = async function (ctx, fileFieldName, seal = null, kind = null) {
|
||||||
if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null;
|
if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null;
|
||||||
const entry = ctx.request.files[fileFieldName];
|
const entry = ctx.request.files[fileFieldName];
|
||||||
const first = Array.isArray(entry) ? entry[0] : entry;
|
const first = Array.isArray(entry) ? entry[0] : entry;
|
||||||
return storeUploadedFile(first, seal);
|
return storeUploadedFile(first, seal, kind);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) {
|
const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) {
|
||||||
|
|
@ -507,5 +646,5 @@ const serveBlob = async function (ctx) {
|
||||||
sendBlobBuffer(ctx, buffer);
|
sendBlobBuffer(ctx, buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, stripImageMetadata };
|
module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, WrongFileTypeError, stripImageMetadata, stripFileMetadata };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,15 +94,6 @@ const assertKind = (kind) => {
|
||||||
return k;
|
return k;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastArg = (args, n) => args[args.length - n];
|
|
||||||
|
|
||||||
const kindFromArgs = (args) => {
|
|
||||||
const k = String(lastArg(args, 1) || "").trim();
|
|
||||||
return assertKind(k);
|
|
||||||
};
|
|
||||||
|
|
||||||
const idFromArgs = (args) => String(lastArg(args, 1) || "").trim();
|
|
||||||
|
|
||||||
exports.getFavoriteSet = async (kind) => {
|
exports.getFavoriteSet = async (kind) => {
|
||||||
const k = assertKind(kind);
|
const k = assertKind(kind);
|
||||||
const data = await readAll();
|
const data = await readAll();
|
||||||
|
|
@ -142,15 +133,4 @@ exports.removeFavorite = async (kind, id) =>
|
||||||
await writeAll(data);
|
await writeAll(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.getFavoritesSet = async (...args) => exports.getFavoriteSet(kindFromArgs(args));
|
|
||||||
exports.addToFavorites = async (...args) => {
|
|
||||||
const kind = kindFromArgs(args.slice(0, -1));
|
|
||||||
const id = idFromArgs(args);
|
|
||||||
return exports.addFavorite(kind, id);
|
|
||||||
};
|
|
||||||
exports.removeFromFavorites = async (...args) => {
|
|
||||||
const kind = kindFromArgs(args.slice(0, -1));
|
|
||||||
const id = idFromArgs(args);
|
|
||||||
return exports.removeFavorite(kind, id);
|
|
||||||
};
|
|
||||||
exports.storePath = storePath;
|
exports.storePath = storePath;
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,15 @@ const set = (feedId, name, ts) => {
|
||||||
const has = (feedId) => !!feedId && cache.has(String(feedId))
|
const has = (feedId) => !!feedId && cache.has(String(feedId))
|
||||||
const size = () => cache.size
|
const size = () => cache.size
|
||||||
|
|
||||||
module.exports = { get, set, has, size }
|
const images = new Map()
|
||||||
|
const NULL_IMAGE_RE = /^&0{43}=\.sha256$/
|
||||||
|
|
||||||
|
const image = (feedId) => (feedId && images.get(String(feedId))) || null
|
||||||
|
|
||||||
|
const setImage = (feedId, img) => {
|
||||||
|
const link = typeof img === 'string' ? img : (img && typeof img.link === 'string' ? img.link : null)
|
||||||
|
if (!feedId || !link || !link.startsWith('&') || NULL_IMAGE_RE.test(link)) return
|
||||||
|
images.set(String(feedId), link)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { get, set, has, size, image, setImage }
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const constructive = [
|
||||||
"rejected",
|
"rejected",
|
||||||
"needsSources",
|
"needsSources",
|
||||||
"wrong",
|
"wrong",
|
||||||
|
"outdated",
|
||||||
"lowQuality",
|
"lowQuality",
|
||||||
"confusing",
|
"confusing",
|
||||||
"misleading",
|
"misleading",
|
||||||
|
|
@ -46,7 +47,7 @@ const all = [...positive, ...constructive, ...moderation];
|
||||||
all.positive = positive;
|
all.positive = positive;
|
||||||
all.constructive = constructive;
|
all.constructive = constructive;
|
||||||
all.moderation = moderation;
|
all.moderation = moderation;
|
||||||
const karmaNeutral = ["adultOnly", "nsfw", "triggering", "duplicate", "offTopic", "needsSources"];
|
const karmaNeutral = ["adultOnly", "nsfw", "triggering", "duplicate", "offTopic", "needsSources", "outdated"];
|
||||||
const karmaModeration = [...moderation.filter(c => !karmaNeutral.includes(c)), "propaganda"];
|
const karmaModeration = [...moderation.filter(c => !karmaNeutral.includes(c)), "propaganda"];
|
||||||
all.karma = {
|
all.karma = {
|
||||||
positive,
|
positive,
|
||||||
|
|
@ -79,7 +80,19 @@ all.messageTypes = {
|
||||||
podcast: 'podcastOpinion',
|
podcast: 'podcastOpinion',
|
||||||
podcastEpisode: 'podcastOpinion',
|
podcastEpisode: 'podcastOpinion',
|
||||||
campaign: 'campaignOpinion',
|
campaign: 'campaignOpinion',
|
||||||
logisticsRoute: 'logisticsOpinion'
|
logisticsRoute: 'logisticsOpinion',
|
||||||
|
wikiPage: 'wikiOpinion',
|
||||||
|
map: 'mapOpinion',
|
||||||
|
industryBlueprint: 'blueprintOpinion',
|
||||||
|
job: 'jobOpinion'
|
||||||
};
|
};
|
||||||
|
all.opinable = (c) => !!c && typeof c === 'object' && !!all.messageTypes[c.type] && !c.tribeId && !c.encrypted && !c.encryptedPayload && !c.contentEncrypted
|
||||||
|
&& (c.type !== 'map' || String(c.mapType || '').toUpperCase() === 'OPEN')
|
||||||
|
&& (c.type !== 'job' || String(c.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN')
|
||||||
|
&& (c.type !== 'schoolCourse' || String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE');
|
||||||
|
|
||||||
|
all.hasOpinions = (x) => Object.values((x && x.opinions) || {}).some(n => (Number(n) || 0) > 0);
|
||||||
|
all.MEDIA_LOCKED = 'Cannot replace the file after it has received opinions.';
|
||||||
|
all.assertMediaKept = (item, replacing) => { if (replacing && all.hasOpinions(item)) throw new Error(all.MEDIA_LOCKED); };
|
||||||
|
|
||||||
module.exports = all;
|
module.exports = all;
|
||||||
|
|
|
||||||
|
|
@ -450,14 +450,6 @@ const reportSections = (report) => {
|
||||||
out.push({ kind: 'text', text: report.description });
|
out.push({ kind: 'text', text: report.description });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tpl = report.template && typeof report.template === 'object' ? report.template : {};
|
|
||||||
const tplEntries = Object.entries(tpl).filter(([, v]) => txt(v).trim());
|
|
||||||
if (tplEntries.length) {
|
|
||||||
out.push({ kind: 'blank' });
|
|
||||||
out.push({ kind: 'section', text: 'DETAILS' });
|
|
||||||
for (const [k, v] of tplEntries) out.push({ kind: 'kv', label: humanLabel(k), value: v });
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmations = asList(report.confirmations);
|
const confirmations = asList(report.confirmations);
|
||||||
out.push({ kind: 'blank' });
|
out.push({ kind: 'blank' });
|
||||||
out.push({ kind: 'section', text: 'CONFIRMATIONS' });
|
out.push({ kind: 'section', text: 'CONFIRMATIONS' });
|
||||||
|
|
|
||||||
176
src/backend/pdf_view.js
Normal file
176
src/backend/pdf_view.js
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
|
|
||||||
|
const WORKER_TAG = 'oasis-pdf-view';
|
||||||
|
const MUPDF_URL = pathToFileURL(path.join(__dirname, '..', 'server', 'node_modules', 'mupdf', 'dist', 'mupdf.js')).href;
|
||||||
|
const PDF_BASE_WIDTH = 1600;
|
||||||
|
const PDF_FIT_WIDTH = 1200;
|
||||||
|
const PDF_WEBP_EFFORT = 1;
|
||||||
|
const PDF_ZOOMS = [50, 75, 100, 125, 150, 200, 300];
|
||||||
|
const PDF_ZOOM_DEFAULT = 100;
|
||||||
|
const PDF_PAGE_MAX_PX = 4096;
|
||||||
|
const PDF_CALL_MS = 20000;
|
||||||
|
const PDF_DOCS_BYTES = 128 * 1024 * 1024;
|
||||||
|
const PDF_ROTATIONS = [0, 90, 180, 270];
|
||||||
|
const PDF_FRAME_ID_LEN = 12;
|
||||||
|
const PDF_PAGES_CACHE_BYTES = 64 * 1024 * 1024;
|
||||||
|
const PDF_WORKER_HEAP_MB = 256;
|
||||||
|
const PDF_WEBP_QUALITY = 85;
|
||||||
|
const PDF_MAGIC = Buffer.from('%PDF-');
|
||||||
|
const PDF_MAGIC_WINDOW = 1024;
|
||||||
|
|
||||||
|
const widthOf = (zoom) => (zoom <= PDF_ZOOM_DEFAULT ? PDF_FIT_WIDTH : Math.round(PDF_BASE_WIDTH * zoom / 100));
|
||||||
|
const PDF_WIDTHS = new Set(PDF_ZOOMS.map(widthOf));
|
||||||
|
const zoomOf = (value) => { const z = Number(value); return PDF_ZOOMS.includes(z) ? z : PDF_ZOOM_DEFAULT; };
|
||||||
|
const rotationOf = (value) => { const r = Number(value); return PDF_ROTATIONS.includes(r) ? r : 0; };
|
||||||
|
const looksLikePdf = (buf) => Buffer.isBuffer(buf) && buf.subarray(0, PDF_MAGIC_WINDOW).indexOf(PDF_MAGIC) >= 0;
|
||||||
|
|
||||||
|
if (!isMainThread && workerData === WORKER_TAG) {
|
||||||
|
const docs = new Map();
|
||||||
|
const sizes = new Map();
|
||||||
|
let openBytes = 0;
|
||||||
|
let mupdf = null;
|
||||||
|
const need = (key) => {
|
||||||
|
const doc = docs.get(key);
|
||||||
|
if (!doc) throw new Error('NO_DOC');
|
||||||
|
docs.delete(key);
|
||||||
|
docs.set(key, doc);
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
const describe = (doc) => ({ pages: doc.countPages() });
|
||||||
|
const ops = {
|
||||||
|
open({ key, data }) {
|
||||||
|
const doc = mupdf.Document.openDocument(Buffer.from(data), 'application/pdf');
|
||||||
|
if (doc.needsPassword()) { doc.destroy(); throw new Error('LOCKED'); }
|
||||||
|
docs.set(key, doc);
|
||||||
|
sizes.set(key, data.byteLength);
|
||||||
|
openBytes += data.byteLength;
|
||||||
|
while (openBytes > PDF_DOCS_BYTES && docs.size > 1) {
|
||||||
|
const [oldKey, oldDoc] = docs.entries().next().value;
|
||||||
|
docs.delete(oldKey);
|
||||||
|
openBytes -= sizes.get(oldKey) || 0;
|
||||||
|
sizes.delete(oldKey);
|
||||||
|
try { oldDoc.destroy(); } catch (_) {}
|
||||||
|
}
|
||||||
|
return describe(doc);
|
||||||
|
},
|
||||||
|
info({ key }) { return describe(need(key)); },
|
||||||
|
render({ key, page, width, rotate }) {
|
||||||
|
const doc = need(key);
|
||||||
|
const pg = doc.loadPage(page - 1);
|
||||||
|
try {
|
||||||
|
const [x0, y0, x1, y1] = pg.getBounds();
|
||||||
|
let scale = width / Math.max(1, x1 - x0);
|
||||||
|
scale = Math.min(scale, PDF_PAGE_MAX_PX / Math.max(1, y1 - y0), PDF_PAGE_MAX_PX / Math.max(1, x1 - x0));
|
||||||
|
const pix = pg.toPixmap(mupdf.Matrix.concat(mupdf.Matrix.scale(scale, scale), mupdf.Matrix.rotate(rotate || 0)), mupdf.ColorSpace.DeviceRGB, false, true);
|
||||||
|
try { return { jpeg: pix.asJPEG(PDF_WEBP_QUALITY, false) }; } finally { pix.destroy(); }
|
||||||
|
} finally { pg.destroy(); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
||||||
|
const ready = import(MUPDF_URL).then((m) => {
|
||||||
|
mupdf = m;
|
||||||
|
mupdf.setLog(debug ? (message) => console.error(`[pdf] ${message}`) : () => {});
|
||||||
|
});
|
||||||
|
parentPort.on('message', async ({ id, op, args }) => {
|
||||||
|
try {
|
||||||
|
await ready;
|
||||||
|
if (!ops[op]) throw new Error('BAD_OP');
|
||||||
|
const { data, ...rest } = args || {};
|
||||||
|
if (data && !docs.has(rest.key)) ops.open({ key: rest.key, data });
|
||||||
|
const value = op === 'open' ? describe(need(rest.key)) : ops[op](rest);
|
||||||
|
parentPort.postMessage({ id, value });
|
||||||
|
} catch (e) {
|
||||||
|
parentPort.postMessage({ id, error: String((e && e.message) || e) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let worker = null;
|
||||||
|
let current = null;
|
||||||
|
const queue = [];
|
||||||
|
const stop = () => { const w = worker; worker = null; if (w) w.terminate().catch(() => {}); };
|
||||||
|
const settle = (outcome) => {
|
||||||
|
const job = current;
|
||||||
|
if (!job) return;
|
||||||
|
current = null;
|
||||||
|
clearTimeout(job.timer);
|
||||||
|
if (outcome.error) job.reject(new Error(outcome.error)); else job.resolve(outcome.value);
|
||||||
|
pump();
|
||||||
|
};
|
||||||
|
const start = () => {
|
||||||
|
const w = new Worker(__filename, { workerData: WORKER_TAG, resourceLimits: { maxOldGenerationSizeMb: PDF_WORKER_HEAP_MB } });
|
||||||
|
w.on('message', (msg) => { if (worker === w && current && msg.id === current.id) settle(msg); });
|
||||||
|
const lost = () => { if (worker === w) { worker = null; settle({ error: 'WORKER' }); } };
|
||||||
|
w.on('error', lost);
|
||||||
|
w.on('exit', lost);
|
||||||
|
w.unref();
|
||||||
|
worker = w;
|
||||||
|
};
|
||||||
|
let seq = 0;
|
||||||
|
const pump = () => {
|
||||||
|
if (current || !queue.length) return;
|
||||||
|
if (!worker) start();
|
||||||
|
current = queue.shift();
|
||||||
|
current.timer = setTimeout(() => { stop(); settle({ error: 'TIMEOUT' }); }, PDF_CALL_MS);
|
||||||
|
worker.postMessage({ id: current.id, op: current.op, args: current.args });
|
||||||
|
};
|
||||||
|
const call = (op, args) => new Promise((resolve, reject) => {
|
||||||
|
queue.push({ id: ++seq, op, args, resolve, reject, timer: null });
|
||||||
|
pump();
|
||||||
|
});
|
||||||
|
const loading = new Map();
|
||||||
|
const loadPdf = (key, load) => {
|
||||||
|
if (!loading.has(key)) {
|
||||||
|
loading.set(key, (async () => {
|
||||||
|
const data = await load();
|
||||||
|
if (!data) throw new Error('UNAVAILABLE');
|
||||||
|
if (!looksLikePdf(data)) throw new Error('NOT_PDF');
|
||||||
|
return data;
|
||||||
|
})().finally(() => loading.delete(key)));
|
||||||
|
}
|
||||||
|
return loading.get(key);
|
||||||
|
};
|
||||||
|
const withDoc = async (key, load, op, args = {}) => {
|
||||||
|
try { return await call(op, { key, ...args }); } catch (e) { if (e.message !== 'NO_DOC') throw e; }
|
||||||
|
return call(op, { key, ...args, data: await loadPdf(key, load) });
|
||||||
|
};
|
||||||
|
const pages = new Map();
|
||||||
|
let pagesBytes = 0;
|
||||||
|
const remember = (k, v) => {
|
||||||
|
pages.set(k, v);
|
||||||
|
pagesBytes += v.body.length;
|
||||||
|
for (const [old, entry] of pages) {
|
||||||
|
if (pagesBytes <= PDF_PAGES_CACHE_BYTES) break;
|
||||||
|
pages.delete(old);
|
||||||
|
pagesBytes -= entry.body.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const encode = async ({ jpeg }) => ({ body: Buffer.from(jpeg), type: 'image/jpeg' });
|
||||||
|
const drawing = new Map();
|
||||||
|
const keyOf = (text) => crypto.createHash('sha256').update(String(text)).digest('hex');
|
||||||
|
const frameIdOf = (src) => `pdf-${keyOf(src).slice(0, PDF_FRAME_ID_LEN)}`;
|
||||||
|
module.exports = {
|
||||||
|
PDF_BASE_WIDTH, PDF_ZOOMS, PDF_ZOOM_DEFAULT, PDF_ROTATIONS,
|
||||||
|
widthOf, zoomOf, rotationOf, looksLikePdf, keyOf, frameIdOf,
|
||||||
|
isFrameId: (v) => new RegExp(`^pdf-[0-9a-f]{${PDF_FRAME_ID_LEN}}$`).test(String(v || '')),
|
||||||
|
isPdfWidth: (w) => PDF_WIDTHS.has(Number(w)),
|
||||||
|
info: (key, load) => withDoc(key, load, 'info'),
|
||||||
|
async page(key, load, page, width, rotation) {
|
||||||
|
const r = rotationOf(rotation);
|
||||||
|
const cacheKey = `${key}|${page}|${width}|${r}`;
|
||||||
|
const hit = pages.get(cacheKey);
|
||||||
|
if (hit) { pages.delete(cacheKey); pages.set(cacheKey, hit); return hit; }
|
||||||
|
if (!drawing.has(cacheKey)) {
|
||||||
|
drawing.set(cacheKey, (async () => {
|
||||||
|
const out = await encode(await withDoc(key, load, 'render', { page: Number(page), width: Number(width), rotate: r }), r);
|
||||||
|
remember(cacheKey, out);
|
||||||
|
return out;
|
||||||
|
})().finally(() => drawing.delete(cacheKey)));
|
||||||
|
}
|
||||||
|
return drawing.get(cacheKey);
|
||||||
|
},
|
||||||
|
shutdown: () => { stop(); while (queue.length) queue.shift().reject(new Error('STOPPED')); if (current) settle({ error: 'STOPPED' }); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea } = require("../server/node_modules/hyperaxe");
|
const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea, object } = require("../server/node_modules/hyperaxe");
|
||||||
const i18nBase = require("../client/assets/translations/i18n");
|
const i18nBase = require("../client/assets/translations/i18n");
|
||||||
const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model");
|
const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model");
|
||||||
|
|
||||||
|
|
@ -202,7 +202,20 @@ function renderStyledText(value, opts = {}) {
|
||||||
result.push(audio({ controls: true, class: 'post-audio', src: blobHref(m.blob) }));
|
result.push(audio({ controls: true, class: 'post-audio', src: blobHref(m.blob) }));
|
||||||
} else if (m.type === 'blob-pdf') {
|
} else if (m.type === 'blob-pdf') {
|
||||||
const i18n = getI18n();
|
const i18n = getI18n();
|
||||||
result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || i18n.pdfFallbackLabel || 'PDF'));
|
const pdfLabel = m.name || i18n.pdfFallbackLabel || 'PDF';
|
||||||
|
const clearnet = String(opts.blobPrefix || '').startsWith('/c/');
|
||||||
|
let lang = '';
|
||||||
|
try { lang = clearnet ? ((require('../models/typed_log').requestScope.getStore() || {}).cnLang || '') : ''; } catch (_) {}
|
||||||
|
const fid = require('./pdf_view').frameIdOf(blobHref(m.blob));
|
||||||
|
const query = new URLSearchParams({ src: blobHref(m.blob), title: String(m.name || ''), ...(lang ? { lang } : {}), fid });
|
||||||
|
const holder = (src, cls) => span({ class: 'pdf-holder' },
|
||||||
|
object({ id: fid, data: src, type: 'text/html', class: cls, title: pdfLabel, 'aria-label': pdfLabel }, ''),
|
||||||
|
a({ href: '#', class: 'pdf-unmax', title: i18n.pdfClose || 'Close' }, '✕'));
|
||||||
|
result.push(clearnet
|
||||||
|
? holder(`/c/pdf/embed?${query}`, 'cn-pdf-frame')
|
||||||
|
: opts.blobPrefix
|
||||||
|
? a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, pdfLabel)
|
||||||
|
: holder(`/pdf/embed?${query}`, 'pdf-frame'));
|
||||||
} else if (m.type === 'blob-file') {
|
} else if (m.type === 'blob-file') {
|
||||||
result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || m.blob.slice(1, 9)));
|
result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || m.blob.slice(1, 9)));
|
||||||
} else if (m.type === 'md-mention') {
|
} else if (m.type === 'md-mention') {
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const publicModeGuard = ({ isPublic, onBlocked }) => async (ctx, next) => {
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildCsp = (isClearnet, { frames = false } = {}) => (isClearnet
|
const buildCsp = (isClearnet, { frames = '', framed = false, objects = '' } = {}) => (isClearnet
|
||||||
? [
|
? [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"script-src 'none'",
|
"script-src 'none'",
|
||||||
|
|
@ -76,24 +76,27 @@ const buildCsp = (isClearnet, { frames = false } = {}) => (isClearnet
|
||||||
"img-src 'self' data:",
|
"img-src 'self' data:",
|
||||||
"media-src 'self' blob:",
|
"media-src 'self' blob:",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
"frame-src 'none'",
|
frames ? `frame-src ${frames}` : "frame-src 'none'",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
"object-src 'none'",
|
objects ? `object-src ${objects}` : "object-src 'none'",
|
||||||
"base-uri 'none'",
|
"base-uri 'none'",
|
||||||
"frame-ancestors 'none'"
|
framed ? "frame-ancestors 'self'" : "frame-ancestors 'none'"
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"script-src 'self' http://localhost:3000/js",
|
"script-src 'none'",
|
||||||
"style-src 'self'",
|
"style-src 'self'",
|
||||||
"img-src 'self'",
|
"img-src 'self'",
|
||||||
"media-src 'self' blob:",
|
"media-src 'self' blob:",
|
||||||
"worker-src 'self' blob:",
|
frames ? `frame-src ${frames}` : "frame-src 'none'",
|
||||||
frames ? "frame-src 'self'" : "frame-src 'none'",
|
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
"object-src 'none'",
|
objects ? `object-src ${objects}` : "object-src 'none'",
|
||||||
"base-uri 'none'",
|
"base-uri 'none'",
|
||||||
"frame-ancestors 'none'"
|
framed ? "frame-ancestors 'self'" : "frame-ancestors 'none'"
|
||||||
]).join('; ');
|
]).join('; ');
|
||||||
|
|
||||||
module.exports = { isMsgKey, pickMsgKeys, isLocalPath, safeReturnTo, refererPath, safeRefererRedirect, isClearnetPath, isTrustedRequest, publicModeGuard, buildCsp };
|
const IMAGE_SIZE_MIN = 16;
|
||||||
|
const IMAGE_SIZE_MAX = 1024;
|
||||||
|
const isImageSize = (value) => { const n = Number(value); return Number.isInteger(n) && n >= IMAGE_SIZE_MIN && n <= IMAGE_SIZE_MAX; };
|
||||||
|
|
||||||
|
module.exports = { IMAGE_SIZE_MIN, IMAGE_SIZE_MAX, isImageSize, isMsgKey, pickMsgKeys, isLocalPath, safeReturnTo, refererPath, safeRefererRedirect, isClearnetPath, isTrustedRequest, publicModeGuard, buildCsp };
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: var(--base03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-tag,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-deletion {
|
|
||||||
color: var(--base08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-number,
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-builtin-name,
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-params,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-link {
|
|
||||||
color: var(--base09);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attribute {
|
|
||||||
color: var(--base0A);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-addition {
|
|
||||||
color: var(--base0B);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-section {
|
|
||||||
color: var(--base0D);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag {
|
|
||||||
color: var(--base0E);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
color: var(--base05);
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string, .hljs-link {
|
|
||||||
background: #FFA500;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
@ -672,8 +672,7 @@ a.filter-btn,
|
||||||
.main-column .edit-btn,
|
.main-column .edit-btn,
|
||||||
.main-column .join-btn,
|
.main-column .join-btn,
|
||||||
.main-column .leave-btn,
|
.main-column .leave-btn,
|
||||||
.main-column .buy-btn,
|
.main-column .buy-btn {
|
||||||
.main-column .submit-button {
|
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
overflow-wrap: anywhere !important;
|
overflow-wrap: anywhere !important;
|
||||||
|
|
@ -682,8 +681,7 @@ a.filter-btn,
|
||||||
.main-column .btn,
|
.main-column .btn,
|
||||||
.main-column .create-button,
|
.main-column .create-button,
|
||||||
.main-column .update-btn,
|
.main-column .update-btn,
|
||||||
.main-column .delete-btn,
|
.main-column .delete-btn {
|
||||||
.main-column .submit-button {
|
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
.main-column form {
|
.main-column form {
|
||||||
|
|
|
||||||
|
|
@ -695,22 +695,33 @@ body:has(#oasis-menu-toggle:checked) form.oasis-phone-state { display: none !imp
|
||||||
width: auto !important; min-width: 0; min-height: 44px !important; margin: 0 !important; padding: 4px 6px !important;
|
width: auto !important; min-width: 0; min-height: 44px !important; margin: 0 !important; padding: 4px 6px !important;
|
||||||
font-size: 14px !important; line-height: 1.15 !important; overflow-wrap: anywhere;
|
font-size: 14px !important; line-height: 1.15 !important; overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
/* las fichas van plegadas tras un boton Filtrar (mobile_ui.activityFilter); por
|
/* las fichas van plegadas tras un boton Filtrar (mobile_ui.activityFilter): casilla,
|
||||||
defecto se ve todo y con un filtro puesto el boton dice cual */
|
etiqueta y fichas en la misma caja, que la 1.3.5 mueve a su barra fija (.apk-strip).
|
||||||
.main-column details.activity-filter { margin: 0 0 12px !important; }
|
Por defecto se ve todo; con un filtro puesto el boton dice cual */
|
||||||
.main-column details.activity-filter > summary.activity-filter-toggle {
|
.main-column div.activity-filter-chips.mobile-filter {
|
||||||
display: flex; align-items: center; justify-content: center; min-height: 48px; list-style: none; cursor: pointer;
|
display: block !important; overflow: visible !important; white-space: normal !important;
|
||||||
border: 1px solid var(--ux-line); border-radius: var(--a26-radius, 12px);
|
margin: 0 0 8px !important; padding: 0 !important; width: 100% !important;
|
||||||
|
}
|
||||||
|
.main-column .mobile-filter > input.mobile-filter-toggle { position: absolute !important; opacity: 0 !important; width: 1px !important; height: 1px !important; min-height: 0 !important; pointer-events: none; }
|
||||||
|
.main-column .mobile-filter > label.mobile-filter-btn {
|
||||||
|
display: flex !important; align-items: center; justify-content: center; min-height: 48px; margin: 0 !important; cursor: pointer;
|
||||||
|
border: 1px solid var(--ux-line); border-radius: var(--a26-radius, 12px); background: var(--ux-bg); color: var(--ux-fg);
|
||||||
font-weight: 700; font-size: 16px; text-transform: uppercase; letter-spacing: 1px;
|
font-weight: 700; font-size: 16px; text-transform: uppercase; letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
.main-column details.activity-filter > summary::-webkit-details-marker { display: none; }
|
.main-column .mobile-filter > label.mobile-filter-btn::after { content: "\25BE"; margin-left: 8px; font-size: 16px; }
|
||||||
.main-column details.activity-filter > summary::after { content: "\25BE"; margin-left: 8px; font-size: 16px; }
|
.main-column .mobile-filter > input:checked + label.mobile-filter-btn::after { content: "\25B4"; }
|
||||||
.main-column details.activity-filter[open] > summary { margin-bottom: 10px; }
|
.main-column .mobile-filter.is-filtered > label.mobile-filter-btn { background: var(--ux-accent); color: var(--ux-on-accent); border-color: var(--ux-accent); }
|
||||||
.main-column details.activity-filter[open] > summary::after { content: "\25B4"; }
|
.main-column .mobile-filter .activity-filter-now { margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0; }
|
||||||
.main-column details.activity-filter.is-filtered > summary { background: var(--ux-accent); color: var(--ux-on-accent); border-color: var(--ux-accent); }
|
.main-column .mobile-filter .activity-filter-now::before { content: "\00B7"; margin-right: 8px; }
|
||||||
.main-column .activity-filter-now { margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0; }
|
.main-column .mobile-filter > div.mobile-filter-chips { display: none !important; }
|
||||||
.main-column .activity-filter-now::before { content: "\00B7"; margin-right: 8px; }
|
.main-column .mobile-filter > input:checked ~ div.mobile-filter-chips {
|
||||||
.main-column details.activity-filter div.activity-filter-chips:not(.activity-toolbar-row) { margin: 0 !important; }
|
display: grid !important; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-gap: 8px; margin: 10px 0 0 !important; padding: 0 !important;
|
||||||
|
}
|
||||||
|
.main-column .mobile-filter div.mobile-filter-chips a.activity-chip {
|
||||||
|
display: flex !important; align-items: center; justify-content: center; text-align: center;
|
||||||
|
width: auto !important; min-width: 0; min-height: 44px !important; margin: 0 !important; padding: 4px 6px !important;
|
||||||
|
font-size: 14px !important; line-height: 1.15 !important; overflow-wrap: anywhere; white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* cabecera de cada tarjeta: los botones en una fila arriba a la derecha y la etiqueta
|
/* cabecera de cada tarjeta: los botones en una fila arriba a la derecha y la etiqueta
|
||||||
(FEEDS, SALAS...) debajo a la izquierda; antes compartian fila y la X caia sola */
|
(FEEDS, SALAS...) debajo a la izquierda; antes compartian fila y la X caia sola */
|
||||||
|
|
@ -810,3 +821,38 @@ body .oasis-mobile-topbar .omt-logo img, body .oasis-mobile-topbar .omt-avatar i
|
||||||
background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; border-color: var(--ux-accent) !important;
|
background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; border-color: var(--ux-accent) !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- cabecera de modulo de la 1.3.5 (apkModbar en main_views.js) ----------
|
||||||
|
el dev la pinta en OasisMobile.css, que con el skin Activista no se carga: sin esto
|
||||||
|
sus div heredan el relleno generico y la cabecera ocupaba media pantalla. Los avisos
|
||||||
|
(.apk-banners: EMPEZAR, version nueva) suben por encima con order, sin tocar su HTML */
|
||||||
|
.main-column { display: flex !important; flex-direction: column !important; }
|
||||||
|
.main-column > * { flex: 0 0 auto; min-width: 0; }
|
||||||
|
.main-column > div.apk-banners { order: -1; margin: 0 0 12px !important; padding: 0 !important; }
|
||||||
|
.main-column > div.apk-banners > * { margin: 0 0 10px !important; }
|
||||||
|
.main-column > div.apk-modbar { display: block !important; position: static !important; margin: 0 0 12px !important; padding: 0 !important; background: transparent !important; box-shadow: none !important; }
|
||||||
|
.main-column div.apk-modbar > div.apk-modbar-row { display: flex !important; align-items: center; justify-content: space-between; margin: 0 0 10px !important; padding: 0 !important; }
|
||||||
|
.main-column div.apk-modbar .apk-title { margin: 0 !important; padding: 0 !important; color: var(--ux-fg) !important; }
|
||||||
|
.main-column div.apk-modbar .apk-pin { margin: 0 0 0 10px !important; width: auto !important; flex: 0 0 auto; }
|
||||||
|
.main-column div.apk-modbar > .apk-strip:not(.mobile-filter) { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; margin: 0 0 10px !important; padding: 0 0 4px !important; }
|
||||||
|
.main-column div.apk-modbar > .apk-strip:not(.mobile-filter) > * { flex: 0 0 auto !important; width: auto !important; margin: 0 8px 0 0 !important; white-space: nowrap !important; }
|
||||||
|
.main-column div.apk-modbar > form.filter-box { margin: 0 0 8px !important; }
|
||||||
|
.main-column div.apk-modbar > div.apk-modbar-foot { margin: 0 !important; padding: 0 !important; }
|
||||||
|
.main-column div.apk-modbar div.module-stats-line { margin: 0 !important; padding: 0 !important; font-size: 13px; color: var(--ux-muted) !important; }
|
||||||
|
.main-column div.apk-modbar div.module-stats-line > span { margin-right: 12px; }
|
||||||
|
/* la seccion que se queda vacia cuando la cabecera se lleva las fichas y el buscador */
|
||||||
|
.main-column > section:empty, .main-column div.activity-toolbar-row:empty { display: none !important; }
|
||||||
|
.main-column > section > div.activity-toolbar-row:only-child:empty { display: none !important; }
|
||||||
|
|
||||||
|
/* cabecera de tarjeta de la 1.3.5: autoria a la izquierda (nombre cortado con ...) y
|
||||||
|
el menu ⋯ y la flecha juntos a la derecha. Pisa lo de la 1.3.3 (columna) */
|
||||||
|
.main-column div.card-header.activity-card-header.card-top { flex-direction: row !important; flex-wrap: nowrap !important; align-items: flex-start !important; }
|
||||||
|
.main-column div.card-header.card-top > div.card-byline { display: flex !important; flex-wrap: wrap !important; align-items: center; flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; margin: 0 !important; padding: 0 !important; }
|
||||||
|
.main-column div.card-byline > a.user-link { display: block !important; max-width: calc(100% - 34px) !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; margin: 0 0 0 8px !important; }
|
||||||
|
.main-column div.card-byline > .card-byline-time, .main-column div.card-byline > div.card-chips-row { flex: 0 0 auto; margin: 4px 8px 0 0 !important; }
|
||||||
|
.main-column div.card-header.card-top > div.content-actions { order: 2 !important; flex: 0 0 auto !important; width: auto !important; display: flex !important; flex-wrap: nowrap !important; justify-content: flex-end !important; margin: 0 0 0 8px !important; }
|
||||||
|
.main-column div.content-actions > details.card-menu { width: auto !important; flex: 0 0 auto !important; margin: 0 8px 0 0 !important; }
|
||||||
|
.main-column details.card-menu > summary.card-menu-toggle { min-width: 44px !important; height: 44px !important; }
|
||||||
|
/* el menu desplegado con los colores del skin y filas de 48px */
|
||||||
|
.main-column details.card-menu > div.card-menu-items { background: var(--ux-bg) !important; border: 1px solid var(--ux-line) !important; color: var(--ux-fg) !important; min-width: 220px; }
|
||||||
|
.main-column div.card-menu-items > * { min-height: 48px; }
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,15 @@ input[type="text"], input[type="email"], input[type="password"], textarea {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > br { display: none }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(label, span) { display: block; margin: 0 0 6px 0 }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(h2, h3, p) { margin: 0 0 16px 0 }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(input:not([type="hidden"], [type="checkbox"], [type="radio"], [type="submit"], [type="button"], .logistics-short), select:not(.logistics-price-unit), textarea) { display: block; box-sizing: border-box; width: 100%; max-width: 720px; margin: 0 0 16px 0 }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > input:is([type="checkbox"], [type="radio"]) { display: block; width: auto; margin: 0 0 16px 0 }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(.logistics-short, .logistics-price-unit) { margin: 0 8px 16px 0 }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(div, table):not(.map-form-row, .map-toolbar, .map-form-map-slot) { max-width: 720px; box-sizing: border-box; margin-bottom: 16px }
|
||||||
|
section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > button { margin-top: 0 }
|
||||||
|
|
||||||
/* Body and overall page styles */
|
/* Body and overall page styles */
|
||||||
html {
|
html {
|
||||||
color: #FFA500;
|
color: #FFA500;
|
||||||
|
|
@ -1602,7 +1611,58 @@ button.create-button:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Documents */
|
/* Documents */
|
||||||
|
.pdf-holder {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.pdf-frame {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 80vh;
|
||||||
|
min-height: 520px;
|
||||||
|
border: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.pdf-frame:target {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100000;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: none;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.pdf-unmax {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pdf-frame:target + .pdf-unmax {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 18px;
|
||||||
|
z-index: 100001;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #eee;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.pdf-embed-root,
|
||||||
|
body.pdf-embed {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body.pdf-embed {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
|
|
@ -1611,42 +1671,116 @@ button.create-button:hover {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
.pdf-embedded {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
.pdf-canvas-wrap {
|
.pdf-canvas-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
height: 420px;
|
height: 420px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.pdf-wrap-scroll {
|
||||||
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.pdf-embedded .pdf-canvas-wrap {
|
||||||
.pdf-viewer-container canvas {
|
flex: 1 1 auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.pdf-page-image {
|
||||||
display: block;
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
.pdf-z50 {
|
||||||
.pdf-viewer-container .pdf-controls {
|
max-width: 50%;
|
||||||
margin-top: 1rem;
|
max-height: 50%;
|
||||||
}
|
}
|
||||||
|
.pdf-z75 {
|
||||||
.pdf-viewer-container button {
|
max-width: 75%;
|
||||||
|
max-height: 75%;
|
||||||
|
}
|
||||||
|
.pdf-z100 {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.pdf-z125 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.pdf-z150 {
|
||||||
|
width: 150%;
|
||||||
|
}
|
||||||
|
.pdf-z200 {
|
||||||
|
width: 200%;
|
||||||
|
}
|
||||||
|
.pdf-z300 {
|
||||||
|
width: 300%;
|
||||||
|
}
|
||||||
|
.pdf-viewer-container .pdf-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.pdf-viewer-container .pdf-controls a,
|
||||||
|
.pdf-viewer-container .pdf-controls span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 4px 4px 0;
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
.pdf-viewer-container .pdf-controls a:hover {
|
||||||
.pdf-viewer-container button:hover {
|
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
.pdf-viewer-container .pdf-controls .pdf-off {
|
||||||
.pdf-viewer-container span {
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.pdf-message {
|
||||||
|
padding: 12px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
margin: 0 1rem;
|
text-align: center;
|
||||||
font-family: monospace;
|
}
|
||||||
|
.pdf-maximized {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.pdf-maximized .pdf-canvas-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
.pdf-maximized .pdf-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CVs */
|
/* CVs */
|
||||||
|
|
@ -2098,6 +2232,51 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
|
||||||
right: 112px;
|
right: 112px;
|
||||||
font-size: 1.15em;
|
font-size: 1.15em;
|
||||||
}
|
}
|
||||||
|
.lightbox-prev,
|
||||||
|
.lightbox-next {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.lightbox-prev {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
.lightbox-next {
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
.media-download-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.edited-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.media-lock-note {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Opinions */
|
/* Opinions */
|
||||||
.voting-buttons {
|
.voting-buttons {
|
||||||
|
|
@ -2614,15 +2793,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-comments {
|
|
||||||
background: #2f2f2f;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.market-comments .votations-comment-card {
|
|
||||||
background: #3a3a3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-strong {
|
.countdown-strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -3169,12 +3340,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
|
||||||
color: #ffd740;
|
color: #ffd740;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-footer {
|
|
||||||
font-size: 0.85em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: #ffd740;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-link {
|
.date-link {
|
||||||
color: #ffd740;
|
color: #ffd740;
|
||||||
|
|
@ -3799,8 +3964,6 @@ a.pm-btn:hover { text-decoration: none }
|
||||||
.card-field-stacked .card-value{white-space:pre-wrap;word-break:break-word}
|
.card-field-stacked .card-value{white-space:pre-wrap;word-break:break-word}
|
||||||
.card-field-rich{flex-direction:column;align-items:flex-start}
|
.card-field-rich{flex-direction:column;align-items:flex-start}
|
||||||
.card-field-rich .card-value{white-space:pre-wrap;word-break:break-word;margin-top:4px}
|
.card-field-rich .card-value{white-space:pre-wrap;word-break:break-word;margin-top:4px}
|
||||||
.report-template{margin-top:12px;padding:12px;background:rgba(255,255,255,0.05);border-radius:8px}
|
|
||||||
.report-template .card-label{white-space:nowrap}
|
|
||||||
|
|
||||||
@media (max-width: 420px){
|
@media (max-width: 420px){
|
||||||
.card-field{ flex-direction:column; }
|
.card-field{ flex-direction:column; }
|
||||||
|
|
@ -4311,6 +4474,7 @@ a.pm-btn:hover { text-decoration: none }
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
.tribe-side-actions.report-confirm-actions { justify-content: center }
|
||||||
|
|
||||||
.tribe-side-actions form {
|
.tribe-side-actions form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -6927,8 +7091,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)}
|
||||||
.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px}
|
.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px}
|
||||||
.blogs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px}
|
.blogs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px}
|
||||||
.jobs-grid .tribe-card,.tribe-grid .tribe-card{margin-bottom:0}
|
.jobs-grid .tribe-card,.tribe-grid .tribe-card{margin-bottom:0}
|
||||||
.tribe-card .card-comments-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px}
|
|
||||||
.tribe-card .card-comments-summary .inline-form{margin:0}
|
|
||||||
.jobs-info-table{table-layout:auto}
|
.jobs-info-table{table-layout:auto}
|
||||||
.jobs-info-table .tribe-info-label{width:1%;white-space:nowrap}
|
.jobs-info-table .tribe-info-label{width:1%;white-space:nowrap}
|
||||||
.jobs-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere}
|
.jobs-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere}
|
||||||
|
|
@ -6940,14 +7102,13 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)}
|
||||||
.job-price-line{margin:0;font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace}
|
.job-price-line{margin:0;font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace}
|
||||||
.job-card-view-btn{margin-top:8px}
|
.job-card-view-btn{margin-top:8px}
|
||||||
.job-card-view-btn form{margin:0}
|
.job-card-view-btn form{margin:0}
|
||||||
.vote-comments-section,.comments-count,.comment-form-wrapper,.comments-list,.vote-buttons-block,.vote-buttons-row,.voting-buttons,.vote-table{border:none;background:transparent;padding:0;margin:0}
|
.vote-comments-section,.comment-form-wrapper,.comments-list,.vote-buttons-block,.vote-buttons-row,.voting-buttons,.vote-table{border:none;background:transparent;padding:0;margin:0}
|
||||||
.vote-buttons-block{display:flex;flex-direction:column;gap:8px}
|
.vote-buttons-block{display:flex;flex-direction:column;gap:8px}
|
||||||
.vote-buttons-row{display:flex;gap:8px;flex-wrap:wrap}
|
.vote-buttons-row{display:flex;gap:8px;flex-wrap:wrap}
|
||||||
.voting-buttons{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap}
|
.voting-buttons{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap}
|
||||||
.vote-comments-section{margin-top:16px}
|
.vote-comments-section{margin-top:16px}
|
||||||
.comment-form-wrapper{margin-top:12px}
|
.comment-form-wrapper{margin-top:12px}
|
||||||
.comments-list{margin-top:12px}
|
.comments-list{margin-top:12px}
|
||||||
.comments-count{margin-bottom:8px}
|
|
||||||
.title-with-chip{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important}
|
.title-with-chip{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important}
|
||||||
.title-with-chip h2{margin:0}
|
.title-with-chip h2{margin:0}
|
||||||
.pm-exposition-icon{font-size:14px;line-height:1}
|
.pm-exposition-icon{font-size:14px;line-height:1}
|
||||||
|
|
@ -6955,7 +7116,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)}
|
||||||
.shop-title-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;padding:0;background:transparent;border-radius:0;box-shadow:none}
|
.shop-title-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;padding:0;background:transparent;border-radius:0;box-shadow:none}
|
||||||
.shop-title-row h2{margin:0}
|
.shop-title-row h2{margin:0}
|
||||||
.card-chips-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none}
|
.card-chips-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none}
|
||||||
.tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn,.tribe-card-body .card-comments-summary,.tribe-card-body .tribe-card-members{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none}
|
.tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none}
|
||||||
.tribe-card-body .job-price-line{margin:4px 0}
|
.tribe-card-body .job-price-line{margin:4px 0}
|
||||||
.tribe-card-body .job-card-view-btn{margin-top:8px;margin-bottom:0}
|
.tribe-card-body .job-card-view-btn{margin-top:8px;margin-bottom:0}
|
||||||
.transfer-block-card{border:none;background:transparent;padding:0;margin:0 0 12px 0}
|
.transfer-block-card{border:none;background:transparent;padding:0;margin:0 0 12px 0}
|
||||||
|
|
@ -7316,9 +7477,8 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol
|
||||||
.industry-blueprint-card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0}
|
.industry-blueprint-card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0}
|
||||||
.industry-card-wrap{position:relative;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0;background:transparent;box-shadow:none}
|
.industry-card-wrap{position:relative;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0;background:transparent;box-shadow:none}
|
||||||
.industry-card-wrap .industry-blueprint-card{border:none;background:transparent;padding:0;margin:0;box-shadow:none;border-radius:0}
|
.industry-card-wrap .industry-blueprint-card{border:none;background:transparent;padding:0;margin:0;box-shadow:none;border-radius:0}
|
||||||
.industry-card-wrap > .card-header{position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1}
|
.industry-card-wrap > .card-header:not(.card-top){position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1}
|
||||||
.industry-card-wrap > .card-header > span:empty{display:none}
|
.industry-card-wrap > .industry-blueprint-card{padding:0}
|
||||||
.industry-card-wrap > .industry-blueprint-card{padding:0 96px 0 0}
|
|
||||||
.industry-actions form,.industry-actions .project-control-form--status{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center;gap:10px}
|
.industry-actions form,.industry-actions .project-control-form--status{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center;gap:10px}
|
||||||
.industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0}
|
.industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0}
|
||||||
.industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box}
|
.industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box}
|
||||||
|
|
@ -7459,7 +7619,6 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol
|
||||||
.content-actions .spread-form{margin:0;display:inline-flex}
|
.content-actions .spread-form{margin:0;display:inline-flex}
|
||||||
|
|
||||||
.blog-card-body,
|
.blog-card-body,
|
||||||
.blog-detail-body,
|
|
||||||
.mention-card-body {
|
.mention-card-body {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
|
@ -7490,14 +7649,6 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog-card-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 6px 0;
|
|
||||||
border: none !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-allow-comments {
|
.blog-allow-comments {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
@ -8865,6 +9016,8 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.graphos-lightbox .lightbox-close{position:fixed;top:14px;right:22px;z-index:10002;font-size:28px;text-decoration:none}
|
.graphos-lightbox .lightbox-close{position:fixed;top:14px;right:22px;z-index:10002;font-size:28px;text-decoration:none}
|
||||||
.live-lightbox .lightbox-image-link{display:flex;align-items:center;justify-content:center;max-width:100%;max-height:100%;padding:0;margin:0;border:0;background:none}
|
.live-lightbox .lightbox-image-link{display:flex;align-items:center;justify-content:center;max-width:100%;max-height:100%;padding:0;margin:0;border:0;background:none}
|
||||||
.shop-buy-form,.market-item.actions,.market-bid-form,.market-buy-form{border:0;background:transparent;box-shadow:none;padding:0}
|
.shop-buy-form,.market-item.actions,.market-bid-form,.market-buy-form{border:0;background:transparent;box-shadow:none;padding:0}
|
||||||
|
.pending-media{display:flex;align-items:center;justify-content:center;min-height:120px;max-width:480px;padding:16px;border:1px dashed #8a8a8a;border-radius:10px;background:rgba(128,128,128,0.15);color:#a0a0a0;text-align:center;box-sizing:border-box;margin:8px 0}
|
||||||
|
.buy-box{border:1px solid rgba(255,165,0,0.35);border-radius:8px;padding:14px 16px;margin:0 0 16px 0;background:rgba(255,255,255,0.03)}
|
||||||
.project-progress-block .confirmations-progress{width:100%}
|
.project-progress-block .confirmations-progress{width:100%}
|
||||||
.tags-list .tag-table th:last-child,.tags-list .tag-table td:last-child{width:1%;white-space:nowrap;text-align:right}
|
.tags-list .tag-table th:last-child,.tags-list .tag-table td:last-child{width:1%;white-space:nowrap;text-align:right}
|
||||||
.stats-block .tag-table,.stats-card .tag-table{width:100%;border-collapse:collapse;table-layout:fixed}
|
.stats-block .tag-table,.stats-card .tag-table{width:100%;border-collapse:collapse;table-layout:fixed}
|
||||||
|
|
@ -8924,3 +9077,81 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.live-lightbox.live-fullscreen{background:#000;z-index:10050}
|
.live-lightbox.live-fullscreen{background:#000;z-index:10050}
|
||||||
.live-fullscreen .lightbox-image-link{display:flex;align-items:center;justify-content:center;width:100vw;height:100vh}
|
.live-fullscreen .lightbox-image-link{display:flex;align-items:center;justify-content:center;width:100vw;height:100vh}
|
||||||
.live-fullscreen .lightbox-image{width:100vw;height:100vh;max-width:100vw;max-height:100vh;object-fit:contain;border-radius:0}
|
.live-fullscreen .lightbox-image{width:100vw;height:100vh;max-width:100vw;max-height:100vh;object-fit:contain;border-radius:0}
|
||||||
|
.graphos-zoom-icon{font-size:1.5em;line-height:1;vertical-align:0.06em}
|
||||||
|
.card-top{display:flex;flex-wrap:nowrap;align-items:center;justify-content:space-between;gap:8px;margin:0 0 6px !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important}
|
||||||
|
.card-byline{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;flex:1 1 auto;min-width:0;margin:0 !important;font-size:12px;line-height:16px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important}
|
||||||
|
.card-byline-avatar{display:inline-flex;flex:0 0 auto;background:transparent !important;border:0 !important;padding:0 !important}
|
||||||
|
.card-byline-avatar img{display:block;width:24px;height:24px;border-radius:50%;object-fit:cover;margin:0;border:0}
|
||||||
|
.card-byline a.user-link{display:inline;background:transparent !important;border:0 !important;padding:0 !important;margin:0;border-radius:0;color:#FFD700 !important;font-size:12px;font-weight:700;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:100%;box-shadow:none !important}
|
||||||
|
.card-byline a.user-link:hover{text-decoration:underline;background:transparent !important}
|
||||||
|
.card-byline-time{opacity:.65;white-space:nowrap}
|
||||||
|
.card-byline-time::before{content:"\2022";margin-right:6px}
|
||||||
|
.card-byline .card-chips-row{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0 !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important}
|
||||||
|
.card-byline .pm-exposition-chip{font-size:11px;padding:1px 8px}
|
||||||
|
.card-byline :is(.emergency-severity,.edited-chip,.eco-tax-chip){font-size:11px;padding:1px 8px;margin:0;line-height:16px}
|
||||||
|
.card-byline :is(.edited-badge,.votations-comment-date){font-size:12px;opacity:.65;margin:0}
|
||||||
|
.card-top>.content-actions{display:flex;flex:0 0 auto;flex-wrap:nowrap;align-items:center;gap:4px;margin:0 0 0 auto !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important}
|
||||||
|
.card-top .btn-singleview.btn-content{min-width:32px;height:32px;padding:0 8px;font-size:.95em}
|
||||||
|
.card-menu{position:relative}
|
||||||
|
.card-menu>.card-menu-toggle.btn-singleview{list-style:none;min-width:32px;height:32px;padding:0 6px;font-size:1.3em;border-radius:50%}
|
||||||
|
.card-menu[open]>.card-menu-toggle::before{content:"";position:fixed;inset:0;z-index:29;cursor:default}
|
||||||
|
.card-menu[open]{z-index:40}
|
||||||
|
.card-top .spread-form{display:inline-flex;align-items:center;margin:0;padding:0}
|
||||||
|
.card-top .spread-form .btn-singleview{box-sizing:border-box;height:32px;min-width:32px;margin:0;padding:0 12px;gap:6px;border-radius:16px;font-size:.95em;line-height:1}
|
||||||
|
.card-top .spread-form .btn-singleview .btn-count{line-height:1}
|
||||||
|
.card-top>.content-actions>*{margin-top:0;margin-bottom:0}
|
||||||
|
.card-menu-toggle::-webkit-details-marker{display:none}
|
||||||
|
.card-menu-toggle::marker{content:""}
|
||||||
|
.card-menu-items{position:absolute;right:0;top:calc(100% + 4px);z-index:30;display:flex;flex-direction:column;gap:0;min-width:200px;margin:0 !important;padding:6px 0 !important;background:#1e1f23;border:1px solid #333;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,.45);overflow:visible !important}
|
||||||
|
.card-menu-items .content-action-form,.card-menu-items .spread-form{display:block;margin:0;background:transparent !important;border:0 !important;padding:0 !important}
|
||||||
|
.card-menu-items .btn-singleview{width:100%;justify-content:flex-start;gap:12px;height:40px;margin:0;padding:0 16px;border-radius:0;font-size:1em;background:transparent !important}
|
||||||
|
.card-menu-items .btn-singleview:hover{background:rgba(128,128,128,.2) !important}
|
||||||
|
.card-menu-items .btn-singleview:has(>.btn-icon:only-child){width:100%;min-width:0;height:40px;padding:0 16px;flex-shrink:0}
|
||||||
|
.card-menu-items .btn-singleview .btn-icon{flex:0 0 18px;width:18px;height:18px}
|
||||||
|
.card-menu-items .btn-singleview::after{content:attr(title);font-size:13px;white-space:nowrap}
|
||||||
|
div:has(.card-menu[open]){overflow:visible !important}
|
||||||
|
div:has(>.card-top):not(.tribe-side){padding:8px 16px 12px !important;border-radius:16px !important}
|
||||||
|
.podcast-episode-row .podcast-episode-main{padding:0 !important;border-radius:0 !important}
|
||||||
|
:is(.campaign-update,.logistics-booking,.logistics-rating):has(>.card-top){border-radius:0 !important}
|
||||||
|
:where(:is(div,li):has(>.card-top):not(.tribe-side)) div{margin:0 0 8px;padding:0;border:0;border-radius:0;background-color:transparent;box-shadow:none}
|
||||||
|
:where(:is(div,li):has(>.card-top):not(.tribe-side)) :is(.card-section,.card-body){margin:0 !important;padding:0 !important;border:0 !important;border-radius:0 !important;background:transparent !important;box-shadow:none !important}
|
||||||
|
:is(div,li):has(>.card-top):not(.tribe-side){font-family:'Inter','Roboto','Segoe UI','Helvetica Neue',Arial,sans-serif;font-size:14px;line-height:20px;font-weight:400}
|
||||||
|
:where(:is(div,li):has(>.card-top):not(.tribe-side)) :is(p,.blog-card-text,.feed-text,.post-text){font-size:14px;line-height:20px;font-weight:400}
|
||||||
|
:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title){font-size:18px;font-weight:600;line-height:24px;letter-spacing:0;margin:4px 0 6px}
|
||||||
|
:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title) a{text-decoration:none}
|
||||||
|
:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title) a:hover{text-decoration:underline}
|
||||||
|
.card-engage{display:flex !important;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:8px;margin:8px 0 0 !important}
|
||||||
|
.card-engage>*{flex:0 0 auto;margin:0 !important}
|
||||||
|
.card-engage>details[open],.card-engage>details[open]~details,.card-engage>.feed-votes-summary{flex:1 0 100%}
|
||||||
|
.card-engage>details:not([open]){width:auto}
|
||||||
|
.card-meta-row{gap:8px}
|
||||||
|
.card-engage .opinions-summary,.card-engage .comments-summary,.card-meta-row .opinions-summary,.card-meta-row .comments-summary-link,.card-meta-row .comments-summary,.share-summary-link{display:inline-flex !important;align-items:center;gap:6px;width:auto !important;max-width:none;box-sizing:border-box;height:32px;padding:0 12px !important;border-radius:5px !important;font-size:12px;font-weight:600;line-height:30px;text-decoration:none}
|
||||||
|
.share-summary-link{border:1px solid rgba(128,128,128,.5);background:rgba(128,128,128,.12);color:inherit}
|
||||||
|
.share-summary-link:hover{background:rgba(128,128,128,.25);text-decoration:none}
|
||||||
|
.card-meta-row>.feed-votes-summary{flex:1 0 100%;order:-1;margin:0}
|
||||||
|
.card-share{display:inline-flex;flex:0 0 auto;background:transparent !important;border:0 !important;padding:0 !important}
|
||||||
|
.share-summary-icon{margin:0}
|
||||||
|
.tribe-section-nav{gap:6px;padding:10px 12px !important;margin:0 0 12px !important;overflow:visible}
|
||||||
|
.tribe-section-group{display:contents}
|
||||||
|
.tribe-section-group>form:last-child{margin-right:12px}
|
||||||
|
.tribe-section-nav .filter-btn{margin:0}
|
||||||
|
.wallet-error-actions{display:flex;flex-wrap:wrap;gap:10px;margin:12px 0 4px;padding:0;border:0;background:none;box-shadow:none;overflow:visible}
|
||||||
|
.wallet-error-actions .update-download-link{display:inline-block;padding:10px 18px;background:#FFA500;border:0;border-radius:6px;color:#000;font-size:14px;font-weight:bold;text-decoration:none}
|
||||||
|
.wallet-error-actions .update-download-link:hover{background:#FFD700;color:#000;text-decoration:none}
|
||||||
|
.tribe-card:has(>.card-top)>.tribe-card-body{padding-bottom:0}
|
||||||
|
.tribe-card:has(>.card-top)>.tribe-card-body>:last-child{margin-bottom:0}
|
||||||
|
.bookmark-title .card-title-link:not([href^="/tribe/"]){color:#ff6600}
|
||||||
|
.blockexplorer-search{margin:12px 0 16px;padding:10px 12px}
|
||||||
|
.blockexplorer-search-form :is(.blockexplorer-search-row,.blockexplorer-search-pair,.blockexplorer-search-dates,.blockexplorer-search-actions,.filter-box__controls){margin:0;padding:0;border:0;background:transparent;box-shadow:none;overflow:visible}
|
||||||
|
.blockexplorer-search-form .blockexplorer-search-row{flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px}
|
||||||
|
.blockexplorer-search-form .blockexplorer-search-pair,.blockexplorer-search-form .blockexplorer-search-dates{gap:8px}
|
||||||
|
.blockexplorer-search-form .blockexplorer-search-input{min-width:180px;margin:0}
|
||||||
|
.tribe-main>.media-download-btn{align-self:flex-start}
|
||||||
|
.tribe-side-actions .update-btn.danger-btn{border-color:#d9534f;color:#d9534f;background:transparent}
|
||||||
|
.tribe-side-actions .update-btn.danger-btn:hover{background:#d9534f;color:#000}
|
||||||
|
.detail-share{display:flex;justify-content:flex-start;margin:4px 0 !important;padding:0 !important;border:0 !important;background:transparent !important;box-shadow:none !important;overflow:visible}
|
||||||
|
.detail-share .share-summary-link{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border:1px solid rgba(128,128,128,.5);border-radius:5px;background:rgba(128,128,128,.12);font-size:12px;font-weight:600;line-height:30px;text-decoration:none;color:inherit}
|
||||||
|
.detail-share .share-summary-link:hover{background:rgba(128,128,128,.25);text-decoration:none}
|
||||||
|
|
||||||
|
.link-lightbox > .lightbox-close::before { content: ""; position: fixed; inset: 0; z-index: 0; cursor: default; }
|
||||||
|
.link-lightbox > .link-box { position: relative; z-index: 1; }
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
||||||
.btn-singleview.btn-content { background: #FF6F00 !important; color: #F0F0F0 !important; border: 1px solid #FF6F00 !important; }
|
.btn-singleview.btn-content { background: #FF6F00 !important; color: #F0F0F0 !important; border: 1px solid #FF6F00 !important; }
|
||||||
.btn-singleview.btn-content:hover { background: #FF8F00 !important; color: #FFFFFF !important; border-color: #FF8F00 !important; }
|
.btn-singleview.btn-content:hover { background: #FF8F00 !important; color: #FFFFFF !important; border-color: #FF8F00 !important; }
|
||||||
|
.card-menu-items { background: #F0F0F0 !important; border-color: #D0D0D0 !important; }
|
||||||
|
.card-byline a.user-link { color: #FF6F00 !important; }
|
||||||
.btn-back { background: #FF6F00 !important; color: #FFFFFF !important; }
|
.btn-back { background: #FF6F00 !important; color: #FFFFFF !important; }
|
||||||
.btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
.btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
||||||
.block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; }
|
.block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; }
|
||||||
|
|
@ -409,3 +411,7 @@ button.tribe-action-btn { background: transparent !important; border: 1px solid
|
||||||
.oasis-pager-info { color: #888888; }
|
.oasis-pager-info { color: #888888; }
|
||||||
.oasis-pager-btn { background: #FFFFFF; color: #555555 !important; border: 1px solid #E0E0E0; text-decoration: none !important; }
|
.oasis-pager-btn { background: #FFFFFF; color: #555555 !important; border: 1px solid #E0E0E0; text-decoration: none !important; }
|
||||||
.oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #2C2C2C; color: #2C2C2C !important; text-decoration: none !important; }
|
.oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #2C2C2C; color: #2C2C2C !important; text-decoration: none !important; }
|
||||||
|
|
||||||
|
.pdf-viewer-full {
|
||||||
|
background-color: #F9F9F9;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@ div {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(:is(div, li):has(> .card-top):not(.tribe-side)) div {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div .header-content {
|
div .header-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -320,6 +325,7 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; }
|
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; }
|
||||||
.btn-singleview.btn-content { background: #ffa300 !important; color: #1e1f23 !important; border: 1px solid #ffa300 !important; }
|
.btn-singleview.btn-content { background: #ffa300 !important; color: #1e1f23 !important; border: 1px solid #ffa300 !important; }
|
||||||
.btn-singleview.btn-content:hover { background: #FFD700 !important; color: #1e1f23 !important; border-color: #FFD700 !important; }
|
.btn-singleview.btn-content:hover { background: #FFD700 !important; color: #1e1f23 !important; border-color: #FFD700 !important; }
|
||||||
|
.card-menu-items { background: #1e1f23 !important; border-color: #333333 !important; }
|
||||||
.btn-back { background: #21232b !important; color: #ffa300 !important; }
|
.btn-back { background: #21232b !important; color: #ffa300 !important; }
|
||||||
.btn-back:hover { background: #292b36 !important; color: #FFD700 !important; }
|
.btn-back:hover { background: #292b36 !important; color: #FFD700 !important; }
|
||||||
.block-info-table td, .pm-info-table td { border-color: #444 !important; }
|
.block-info-table td, .pm-info-table td { border-color: #444 !important; }
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; }
|
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; }
|
||||||
.btn-singleview.btn-content { background: #00FF00 !important; color: #000000 !important; border: 1px solid #00FF00 !important; }
|
.btn-singleview.btn-content { background: #00FF00 !important; color: #000000 !important; border: 1px solid #00FF00 !important; }
|
||||||
.btn-singleview.btn-content:hover { background: #000000 !important; color: #00FF00 !important; border-color: #00FF00 !important; }
|
.btn-singleview.btn-content:hover { background: #000000 !important; color: #00FF00 !important; border-color: #00FF00 !important; }
|
||||||
|
.card-menu-items { background: #000000 !important; border-color: #00FF00 !important; }
|
||||||
|
.card-byline a.user-link { color: #00FF00 !important; }
|
||||||
.btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; }
|
.btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; }
|
||||||
.btn-back:hover { background: #00FF00 !important; color: #000000 !important; }
|
.btn-back:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||||
.block-info-table td, .pm-info-table td { border-color: #00FF00 !important; }
|
.block-info-table td, .pm-info-table td { border-color: #00FF00 !important; }
|
||||||
|
|
|
||||||
|
|
@ -1220,16 +1220,12 @@ audio, video { max-width: 100% !important; }
|
||||||
.module-table td:first-child a { white-space: normal !important; word-break: keep-all !important; overflow-wrap: normal !important; hyphens: none !important; }
|
.module-table td:first-child a { white-space: normal !important; word-break: keep-all !important; overflow-wrap: normal !important; hyphens: none !important; }
|
||||||
.module-table td, .module-table th { padding: 8px 6px !important; vertical-align: middle !important; }
|
.module-table td, .module-table th { padding: 8px 6px !important; vertical-align: middle !important; }
|
||||||
.module-table td:nth-child(2) p { font-size: .85em !important; margin: 0 !important; }
|
.module-table td:nth-child(2) p { font-size: .85em !important; margin: 0 !important; }
|
||||||
.pdf-canvas-wrap { height: auto !important; max-height: 70vh !important; overflow: auto !important; }
|
.pdf-frame { height: 75vh !important; min-height: 420px !important; }
|
||||||
.pdf-viewer-container { padding: 8px !important; }
|
.pdf-frame:target { height: 100vh !important; }
|
||||||
|
.pdf-viewer-container { padding: 8px !important; margin-bottom: 8px !important; }
|
||||||
|
.pdf-embedded { margin-bottom: 0 !important; }
|
||||||
.pdf-viewer-container .pdf-controls { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; justify-content: center !important; align-items: center !important; margin-top: 8px !important; }
|
.pdf-viewer-container .pdf-controls { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; justify-content: center !important; align-items: center !important; margin-top: 8px !important; }
|
||||||
.pdf-viewer-container button { font-size: 1em !important; padding: 7px 10px !important; min-width: 42px !important; }
|
.pdf-viewer-container .pdf-controls a, .pdf-viewer-container .pdf-controls span { font-size: 1em !important; padding: 7px 10px !important; min-width: 42px !important; margin: 0 !important; text-align: center !important; box-sizing: border-box !important; text-decoration: none !important; }
|
||||||
.pdf-viewer-container span { margin: 0 4px !important; }
|
|
||||||
.pdf-viewer-container { margin-bottom: 8px !important; }
|
|
||||||
.pdf-maximized { position: fixed !important; inset: 0 !important; z-index: 9999 !important; margin: 0 !important; border-radius: 0 !important; background: #111 !important; display: flex !important; flex-direction: column !important; padding: 6px !important; overflow: hidden !important; }
|
|
||||||
.pdf-maximized .pdf-canvas-wrap { flex: 1 1 auto !important; max-height: none !important; height: auto !important; overflow: auto !important; }
|
|
||||||
.pdf-maximized .pdf-controls { flex-shrink: 0 !important; margin: 6px 0 !important; }
|
|
||||||
.pdf-maximized-body { overflow: hidden !important; }
|
|
||||||
|
|
||||||
.main-content .feed-workspace { display: flex !important; flex-direction: column !important; gap: 10px !important; }
|
.main-content .feed-workspace { display: flex !important; flex-direction: column !important; gap: 10px !important; }
|
||||||
.feed-workspace .feed-side { display: block !important; position: static !important; }
|
.feed-workspace .feed-side { display: block !important; position: static !important; }
|
||||||
|
|
@ -1290,10 +1286,8 @@ audio, video { max-width: 100% !important; }
|
||||||
.feed-card .refeed-column { flex-wrap: wrap !important; }
|
.feed-card .refeed-column { flex-wrap: wrap !important; }
|
||||||
.feed-card .refeed-column .muted { flex-basis: 100% !important; text-align: center !important; margin: 4px 0 0 0 !important; }
|
.feed-card .refeed-column .muted { flex-basis: 100% !important; text-align: center !important; margin: 4px 0 0 0 !important; }
|
||||||
.feed-card .card-engage summary, .feed-card .comments-link { min-height: 40px !important; padding: 8px 10px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: .85em !important; }
|
.feed-card .card-engage summary, .feed-card .comments-link { min-height: 40px !important; padding: 8px 10px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: .85em !important; }
|
||||||
.comments-collapse .comments-inline-form { display: flex !important; flex-direction: column !important; gap: 6px !important; padding: 8px !important; }
|
|
||||||
.comments-collapse textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; }
|
.comments-collapse textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; }
|
||||||
.comments-collapse .filter-btn { width: auto !important; align-self: center !important; padding: 6px 18px !important; }
|
.comments-collapse .filter-btn { width: auto !important; align-self: center !important; padding: 6px 18px !important; }
|
||||||
.comments-collapse .comments-view-all { display: block !important; text-align: center !important; padding: 6px !important; font-size: .8em !important; }
|
|
||||||
.comments-collapse .comment-form { display: flex !important; flex-direction: column !important; gap: 6px !important; width: 100% !important; }
|
.comments-collapse .comment-form { display: flex !important; flex-direction: column !important; gap: 6px !important; width: 100% !important; }
|
||||||
.comments-collapse .comment-textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; }
|
.comments-collapse .comment-textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; }
|
||||||
.comments-collapse .comment-file-upload { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; }
|
.comments-collapse .comment-file-upload { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; }
|
||||||
|
|
@ -3411,7 +3405,7 @@ body > .header {
|
||||||
:where(.main-column) section {
|
:where(.main-column) section {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.main-column :is(form, section, div) > br {
|
.main-column :is(form, section, div):has(> :is(input, select, textarea, button, label)) > br {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.main-column .card-field > a.card-value.user-link {
|
.main-column .card-field > a.card-value.user-link {
|
||||||
|
|
@ -3604,6 +3598,10 @@ body > .header {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
|
.main-column .room-live-panel .room-live-head-main > .card-chips-row {
|
||||||
|
justify-content: center !important;
|
||||||
|
margin-bottom: 1em !important;
|
||||||
|
}
|
||||||
.main-column .tribe-main:has(> .room-live-panel) {
|
.main-column .tribe-main:has(> .room-live-panel) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
|
|
@ -4040,3 +4038,127 @@ body > .header {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
.main-column .tribe-side { flex-flow: row wrap !important; justify-content: center !important; align-items: center !important; column-gap: 8px !important; }
|
||||||
|
.main-column .tribe-side > * { flex: 0 0 100% !important; min-width: 0 !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-actions:not(.tribe-side-actions-bottom) { flex-wrap: nowrap !important; justify-content: center !important; gap: 8px !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-actions:not(.tribe-side-actions-bottom) button { font-size: 13px !important; padding: 6px 10px !important; white-space: nowrap !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-actions-bottom { order: 1 !important; flex: 0 0 auto !important; margin: 0 !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-subtribes { order: 2 !important; flex: 0 0 auto !important; margin: 0 !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-subtribes ~ .tribe-side-tags { order: 3 !important; }
|
||||||
|
.main-column .tribe-side > .tribe-side-subtribes { overflow: visible !important; }
|
||||||
|
.main-column .tribe-side > :is(.tribe-side-subtribes, .tribe-side-actions-bottom) :is(form, button) { margin-top: 0 !important; margin-bottom: 0 !important; }
|
||||||
|
.main-column .tribe-side > .tribe-members-count { order: 4 !important; text-align: center !important; }
|
||||||
|
.main-column .tribe-open-invite-panel { text-align: center !important; }
|
||||||
|
.main-column .tribe-open-invite-panel .invite-qr-card { margin-left: auto !important; margin-right: auto !important; }
|
||||||
|
.main-column .tribe-open-invite-panel > form { display: flex !important; justify-content: center !important; }
|
||||||
|
.main-column .tribe-open-invite-panel .tribe-open-invite { text-align: center !important; }
|
||||||
|
.main-column .tribe-open-invite-panel .tribe-open-invite-code { margin-bottom: 6px !important; }
|
||||||
|
.main-column .tribe-open-invite-panel .invite-qr-card { margin-top: 0 !important; margin-bottom: 6px !important; }
|
||||||
|
.main-column .tribe-open-invite-panel > form button { margin-top: 0 !important; }
|
||||||
|
.main-column .pm-card .pm-info-table tr { flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; }
|
||||||
|
.main-column .pm-card .pm-info-table td.card-label { flex: 0 0 auto !important; width: auto !important; }
|
||||||
|
.main-column .pm-card .pm-info-table td.card-value { flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; }
|
||||||
|
.main-column .pm-card .pm-info-table td.card-value > a.user-link { display: block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
|
||||||
|
.main-column .pm-card .pm-info-table, .main-column .pm-card .pm-info-table > tbody { display: flex !important; flex-direction: column !important; }
|
||||||
|
.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell) { order: -1 !important; }
|
||||||
|
.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell) { flex-wrap: wrap !important; align-items: baseline !important; row-gap: 6px !important; }
|
||||||
|
.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell)::after { content: "" !important; order: -1 !important; flex: 0 0 100% !important; height: 0 !important; }
|
||||||
|
.main-column .pm-card .pm-subject-cell { display: contents !important; }
|
||||||
|
.main-column .pm-card .pm-subject-cell > .pm-exposition-chip { order: -2 !important; margin: 0 !important; }
|
||||||
|
.main-column .pm-card .pm-subject-cell > span:not(.pm-exposition-chip) { flex: 1 1 0 !important; min-width: 0 !important; }
|
||||||
|
|
||||||
|
.main-column .card-header.card-top { display: flex !important; flex-wrap: nowrap !important; align-items: flex-start !important; justify-content: space-between !important; gap: 8px !important; width: auto !important; min-height: 0 !important; margin: 0 0 6px !important; padding: 0 !important; }
|
||||||
|
.main-column .card-header.card-top > .card-byline { display: flex !important; flex: 1 1 auto !important; flex-wrap: wrap !important; align-items: center !important; gap: 4px 6px !important; min-width: 0 !important; min-height: 32px !important; width: auto !important; margin: 0 !important; padding: 0 !important; }
|
||||||
|
.main-column .card-header.card-top .card-byline-avatar { display: inline-flex !important; flex: 0 0 auto !important; align-items: center !important; width: 24px !important; height: 32px !important; min-width: 0 !important; padding: 0 !important; margin: 0 !important; }
|
||||||
|
.main-column .card-header.card-top .card-byline-avatar img { width: 24px !important; height: 24px !important; min-height: 0 !important; max-height: 24px !important; border-radius: 50% !important; object-fit: cover !important; margin: 0 !important; }
|
||||||
|
.main-column .card-header.card-top .card-byline a.user-link { display: block !important; flex: 0 1 auto !important; width: auto !important; height: auto !important; min-width: min(72px, 100%) !important; max-width: calc(100% - 150px) !important; padding: 0 !important; margin: 0 !important; font-size: 12px !important; line-height: 16px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
|
||||||
|
.main-column .card-header.card-top > .content-actions { display: flex !important; flex: 0 0 auto !important; flex-wrap: nowrap !important; align-items: center !important; gap: 4px !important; width: auto !important; margin: 0 0 0 auto !important; padding: 0 !important; }
|
||||||
|
.main-column .card-header.card-top > .content-actions > .card-menu { display: block !important; width: auto !important; margin: 0 !important; }
|
||||||
|
.main-column .card-header.card-top > .content-actions .btn-singleview:not(.card-menu-items *) { width: auto !important; min-width: 32px !important; height: 32px !important; margin: 0 !important; }
|
||||||
|
.main-column .card-header.card-top .card-menu > .card-menu-toggle.btn-singleview { background: #2a2a2a !important; border-radius: 50% !important; }
|
||||||
|
.main-column .card-header.card-top .card-menu > .card-menu-items { position: absolute !important; top: calc(100% + 4px) !important; right: 0 !important; left: auto !important; z-index: 700 !important; display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; gap: 0 !important; width: max-content !important; min-width: 200px !important; max-width: calc(100vw - 32px) !important; height: auto !important; margin: 0 !important; padding: 6px 0 !important; background: #1e1f23 !important; border: 1px solid #333 !important; border-radius: 8px !important; box-shadow: 0 6px 18px rgba(0,0,0,.45) !important; }
|
||||||
|
.main-column .card-header.card-top .card-menu-items :is(.content-action-form, .spread-form) { display: block !important; width: 100% !important; margin: 0 !important; padding: 0 !important; background: transparent !important; border: 0 !important; }
|
||||||
|
.main-column .card-header.card-top .card-menu-items .btn-singleview { display: flex !important; align-items: center !important; justify-content: flex-start !important; gap: 12px !important; width: 100% !important; min-width: 0 !important; height: 40px !important; margin: 0 !important; padding: 0 16px !important; border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; }
|
||||||
|
.main-column .card-header.card-top .card-menu-items .btn-singleview::after { content: attr(title); font-size: 13px; white-space: nowrap; }
|
||||||
|
.lightbox:not(.graphos-lightbox, .bank-eco-chart-lightbox, .live-lightbox) > .lightbox-close { right: 16px !important; }
|
||||||
|
.lightbox:not(.graphos-lightbox, .bank-eco-chart-lightbox, .live-lightbox) > .lightbox-download { right: 68px !important; }
|
||||||
|
|
||||||
|
/* APK: barra de modulo compacta y fija */
|
||||||
|
body:has(.apk-modbar), body:has(.apk-modbar) .main-content { overflow-x: clip !important; overflow-y: visible !important; }
|
||||||
|
.main-column > .apk-modbar { position: sticky !important; top: 60px !important; z-index: 800 !important; background: #121212 !important; display: flex !important; flex-direction: column !important; gap: 6px !important; width: auto !important; max-width: none !important; margin: -20px calc(50% - 50vw) 6px !important; padding: 12px calc(50vw - 50%) 6px !important; box-shadow: none !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; min-width: 0 !important; width: 100% !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row > .apk-title { flex: 0 1 auto !important; min-width: 0 !important; margin: 0 !important; padding: 0 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; color: #ffa300 !important; line-height: 1.1 !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row > .tags-header { flex: 0 1 auto !important; margin: 0 !important; padding: 0 !important; border: 0 !important; background: transparent !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row > .tags-header > p { display: none !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row > a { flex: 0 0 auto !important; display: inline-flex !important; align-items: center !important; margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-row > a > .pm-exposition-chip { margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-foot { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; min-width: 0 !important; width: 100% !important; }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > .apk-pin { flex: 0 0 auto !important; margin: 0 0 0 auto !important; }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > form.apk-pin { display: flex !important; padding: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) :is(.apk-pin.create-button, .apk-pin .create-button) { width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 2px 10px !important; font-size: 11.25px !important; font-weight: 700 !important; line-height: 16.875px !important; border-radius: 12px !important; border: 1px solid #ffcc00 !important; background: #ffcc00 !important; color: #111 !important; white-space: nowrap !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-strip { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; overflow-x: auto !important; scrollbar-width: none; gap: 6px !important; width: auto !important; max-width: none !important; margin: 0 calc((100% - 100vw) / 2) !important; padding: 0 calc((100vw - 100%) / 2) !important; border: 0 !important; background: transparent !important; overscroll-behavior: none !important; }
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::before, .main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::after { position: sticky; z-index: 3; flex: 0 0 auto; align-self: stretch; width: 22px; padding-bottom: 3px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; color: #FFD700; font-size: 22px; font-weight: 700; line-height: 1; pointer-events: none; opacity: 0; animation-duration: auto; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: scroll(nearest inline); }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::before { content: "‹"; order: -1; left: calc((100% - 100vw) / 2); margin-right: -28px; justify-content: center; background: #121212; animation-name: apk-strip-left; }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::after { content: "›"; right: calc((100% - 100vw) / 2); margin-left: -28px; justify-content: center; background: #121212; animation-name: apk-strip-right; }
|
||||||
|
@keyframes apk-strip-right { 0%, 92% { opacity: 1; } 100% { opacity: 0; } }
|
||||||
|
@keyframes apk-strip-left { 0% { opacity: 0; } 8%, 100% { opacity: 1; } }
|
||||||
|
}
|
||||||
|
.main-column > .apk-modbar > .apk-strip::-webkit-scrollbar { display: none; }
|
||||||
|
.main-column > .apk-modbar > .apk-strip :is(form, div) { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 6px !important; margin: 0 !important; padding: 0 !important; flex: 0 0 auto !important; width: auto !important; max-width: none !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-strip :is(a, button, label) { flex: 0 0 auto !important; white-space: nowrap !important; margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box .filter-box__input { flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; height: 30px !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; font-size: 14px !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box .filter-box__controls { flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; margin: 0 !important; display: flex !important; flex-direction: row !important; gap: 4px !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box .filter-box__select { height: 30px !important; padding-top: 0 !important; padding-bottom: 0 !important; width: auto !important; max-width: 110px !important; margin: 0 !important; font-size: 13px !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box .filter-box__button { font-size: 0 !important; width: 36px !important; min-width: 36px !important; height: 30px !important; padding: 0 !important; margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box .filter-box__button::after { content: "⌕"; font-size: 22px; line-height: 1; color: #FFD700; font-weight: 700; }
|
||||||
|
.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > .apk-pin.create-button { margin: 0 0 0 auto !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box:has(.apk-more-fields) { flex-wrap: wrap !important; row-gap: 6px !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-toggle { display: none !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-btn { flex: 0 0 auto !important; display: flex !important; align-items: center !important; justify-content: center !important; width: 36px !important; height: 30px !important; margin: 0 !important; padding: 0 !important; font-size: 18px !important; line-height: 1 !important; color: #FFD700 !important; background: #2a2a2a !important; border: 1px solid #444 !important; border-radius: 8px !important; cursor: pointer !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-btn::after { content: "⊕"; }
|
||||||
|
.main-column > .apk-modbar .filter-box:has(.apk-more-toggle:checked) .apk-more-btn::after { content: "⊖"; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields { display: none !important; order: 99 !important; flex: 0 0 100% !important; flex-wrap: wrap !important; gap: 6px !important; margin: 0 !important; padding: 0 !important; border: 0 !important; border-radius: 0 !important; overflow: visible !important; background: transparent !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-toggle:checked ~ .apk-more-fields { display: flex !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > :is(input, select) { background: rgba(255, 255, 255, 0.12) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; border-radius: 4px !important; color: #FFD700 !important; font-size: 14px !important; padding: 0 12px !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > select:not(#apk-z) { -webkit-appearance: none !important; appearance: none !important; box-sizing: border-box !important; height: 32px !important; padding: 0 30px 0 12px !important; border-radius: 4px !important; text-align: left !important; background-color: rgba(255, 255, 255, 0.12) !important; background-image: linear-gradient(45deg, transparent 50%, #FFD700 50%), linear-gradient(135deg, #FFD700 50%, transparent 50%) !important; background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55% !important; background-size: 5px 5px, 5px 5px !important; background-repeat: no-repeat !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > input:not(#apk-z) { box-sizing: border-box !important; height: 32px !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-toggle:checked ~ .apk-more-fields:has(> .apk-more-field) { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 6px 8px !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > .apk-more-field { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 2px !important; min-width: 0 !important; height: auto !important; background: transparent !important; border: 0 !important; border-radius: 0 !important; overflow: visible !important; padding: 0 !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-field > .apk-more-label { flex: 0 0 auto !important; font-size: 11px !important; line-height: 1.2 !important; color: #FFD700 !important; text-align: left !important; opacity: 0.85 !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-field > :is(input, select):not(#apk-z) { flex: 0 0 auto !important; min-width: 0 !important; width: 100% !important; box-sizing: border-box !important; height: 32px !important; margin: 0 !important; padding: 0 8px !important; font-size: 13px !important; color: #FFD700 !important; background-color: rgba(255, 255, 255, 0.12) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; border-radius: 4px !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-field > select:not(#apk-z) { -webkit-appearance: none !important; appearance: none !important; padding: 0 30px 0 12px !important; background-image: linear-gradient(45deg, transparent 50%, #FFD700 50%), linear-gradient(135deg, #FFD700 50%, transparent 50%) !important; background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55% !important; background-size: 5px 5px, 5px 5px !important; background-repeat: no-repeat !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields input:is([type="datetime-local"], [type="date"], [type="time"])::-webkit-calendar-picker-indicator { display: none !important; -webkit-appearance: none !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-field > input::placeholder { color: rgba(255, 215, 0, 0.75) !important; opacity: 1 !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > input::placeholder { color: rgba(255, 215, 0, 0.75) !important; opacity: 1 !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-toggle:checked ~ .filter-box__controls .apk-more-btn, .main-column > .apk-modbar .filter-box:has(.apk-more-toggle:checked) .apk-more-btn { border-color: #FFD700 !important; }
|
||||||
|
.main-column > .apk-modbar .apk-more-fields > * { flex: 1 1 100% !important; min-width: 0 !important; width: auto !important; max-width: none !important; height: 30px !important; margin: 0 !important; font-size: 14px !important; }
|
||||||
|
body .main-column > .apk-modbar > .filter-box input[type="hidden"] { display: none !important; margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .filter-box :is(input.filter-box__input, select, .filter-box__controls, .apk-more-fields > *):not(#apk-z) { margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-xrow { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; scrollbar-width: none; overscroll-behavior: none !important; align-items: center !important; gap: 4px !important; width: auto !important; max-width: none !important; margin: 0 calc((100% - 100vw) / 2) !important; padding: 0 calc((100vw - 100%) / 2) !important; border: 0 !important; background: transparent !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-xrow::-webkit-scrollbar { display: none; }
|
||||||
|
.main-column > .apk-modbar > .apk-xrow > * { flex: 0 0 auto !important; margin: 0 !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-xrow.inbox-exposition > .inbox-filters-label { display: none !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-xrow .inbox-bulk-delete { display: none !important; }
|
||||||
|
.main-column > .apk-banners { display: flex !important; flex-direction: column !important; gap: 6px !important; margin: 0 0 18px !important; }
|
||||||
|
.main-column > .apk-banners:has(+ .apk-stats) { margin-bottom: 8px !important; }
|
||||||
|
.main-column > .apk-banners > * { margin: 0 !important; }
|
||||||
|
.main-column > .apk-banners .welcome-banner-start { background: #FFA300 !important; border-color: #FFA300 !important; color: #111 !important; text-transform: uppercase !important; white-space: nowrap !important; width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 5px 14px !important; font-size: 13px !important; font-weight: 700 !important; line-height: 1.2 !important; border-radius: 14px !important; }
|
||||||
|
.main-column > .apk-modbar > .apk-modbar-foot > .module-stats-line.apk-stats { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; padding: 0 !important; display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 4px 8px !important; font-size: .72em !important; white-space: nowrap !important; text-transform: uppercase !important; background: transparent !important; border: 0 !important; }
|
||||||
|
.main-column .activity-toolbar-row:not(:has(*)) { display: none !important; }
|
||||||
|
.main-column .inbox-exposition { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; scrollbar-width: none; align-items: center !important; gap: 4px !important; margin: 0 0 10px !important; padding: 0 !important; border: 0 !important; background: transparent !important; }
|
||||||
|
.main-column .inbox-exposition > .inbox-filters-label { flex: 0 0 auto !important; margin: 0 !important; font-size: 11.25px !important; }
|
||||||
|
.main-column .inbox-exposition > form { flex: 0 0 auto !important; }
|
||||||
|
.main-column .inbox-exposition::-webkit-scrollbar { display: none; }
|
||||||
|
.main-column .inbox-exposition > form { display: inline-flex !important; margin: 0 !important; padding: 0 !important; width: auto !important; }
|
||||||
|
.main-column .inbox-exposition .btn { flex: 0 0 auto !important; width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 2px 7px !important; font-size: 11.25px !important; font-weight: 400 !important; line-height: 16.875px !important; border-radius: 12px !important; border: 1px solid #555 !important; background: transparent !important; color: #FFD700 !important; white-space: nowrap !important; text-transform: uppercase !important; }
|
||||||
|
.main-column .inbox-exposition .btn.delete-btn { color: #e74c3c !important; border-color: #e74c3c !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-description { order: 1 !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > :is(.tribe-side-actions:not(.tribe-side-actions-bottom), .tribe-open-invite-panel) { order: 3 !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-tags { order: 2 !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-members-count { order: 4 !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-actions-bottom { order: 5 !important; }
|
||||||
|
.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-subtribes { order: 6 !important; }
|
||||||
|
.main-column .tribe-details:has(> .tribe-side):not(#apk-z) { background: transparent !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; }
|
||||||
|
.main-column .tribe-details:has(> .tribe-side):not(#apk-z) > .tribe-side { background: #1e1e1e !important; border-radius: 14px !important; padding: 12px !important; margin: 0 0 12px !important; }
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; }
|
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; }
|
||||||
.btn-singleview.btn-content { background: #B86ADE !important; color: #4B1A72 !important; border: 1px solid #B86ADE !important; }
|
.btn-singleview.btn-content { background: #B86ADE !important; color: #4B1A72 !important; border: 1px solid #B86ADE !important; }
|
||||||
.btn-singleview.btn-content:hover { background: #FFD600 !important; color: #4B1A72 !important; border-color: #FFD600 !important; }
|
.btn-singleview.btn-content:hover { background: #FFD600 !important; color: #4B1A72 !important; border-color: #FFD600 !important; }
|
||||||
|
.card-menu-items { background: #4B1A72 !important; border-color: #5A1A85 !important; }
|
||||||
|
.card-byline a.user-link { color: #FFD600 !important; }
|
||||||
.btn-back { background: #A34AD8 !important; color: #FFFFFF !important; }
|
.btn-back { background: #A34AD8 !important; color: #FFFFFF !important; }
|
||||||
.btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; }
|
.btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; }
|
||||||
.block-info-table td, .pm-info-table td { border-color: #B86ADE !important; }
|
.block-info-table td, .pm-info-table td { border-color: #B86ADE !important; }
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,9 @@ module.exports = {
|
||||||
wfHint_mobile: "The basics for a phone",
|
wfHint_mobile: "The basics for a phone",
|
||||||
workflow_acampada: "Camp",
|
workflow_acampada: "Camp",
|
||||||
workflow_activista: "Activist",
|
workflow_activista: "Activist",
|
||||||
wfHint_activista: "Direct action: call out, coordinate, phone and alert",
|
wfHint_activista: "For a collective: everything in Camp plus campaigns, alerts, polls, calendar and tasks",
|
||||||
|
workflow_minimal: "Minimal",
|
||||||
|
wfHint_minimal: "Direct action: call out, coordinate, phone and alert",
|
||||||
wfNameMobile_activists: "Movement",
|
wfNameMobile_activists: "Movement",
|
||||||
wfModuleCount: "{n} modules",
|
wfModuleCount: "{n} modules",
|
||||||
activityFilter: "Filter",
|
activityFilter: "Filter",
|
||||||
|
|
@ -222,7 +224,9 @@ module.exports = {
|
||||||
wfHint_mobile: "Lo básico para el móvil",
|
wfHint_mobile: "Lo básico para el móvil",
|
||||||
workflow_acampada: "Acampada",
|
workflow_acampada: "Acampada",
|
||||||
workflow_activista: "Activista",
|
workflow_activista: "Activista",
|
||||||
wfHint_activista: "Acción directa: convocar, coordinarse, llamar y avisar",
|
wfHint_activista: "Para un colectivo: lo de Acampada más campañas, alertas, encuestas, calendario y tareas",
|
||||||
|
workflow_minimal: "Mínimo",
|
||||||
|
wfHint_minimal: "Acción directa: convocar, coordinarse, llamar y avisar",
|
||||||
wfNameMobile_activists: "Movimiento",
|
wfNameMobile_activists: "Movimiento",
|
||||||
wfModuleCount: "{n} módulos",
|
wfModuleCount: "{n} módulos",
|
||||||
activityFilter: "Filtrar",
|
activityFilter: "Filtrar",
|
||||||
|
|
@ -369,7 +373,9 @@ module.exports = {
|
||||||
wfHint_mobile: "L'essentiel pour le téléphone",
|
wfHint_mobile: "L'essentiel pour le téléphone",
|
||||||
workflow_acampada: "Campement",
|
workflow_acampada: "Campement",
|
||||||
workflow_activista: "Activiste",
|
workflow_activista: "Activiste",
|
||||||
wfHint_activista: "Action directe : appeler, se coordonner, téléphoner et alerter",
|
wfHint_activista: "Pour un collectif : tout Campement plus campagnes, alertes, sondages, calendrier et tâches",
|
||||||
|
workflow_minimal: "Minimal",
|
||||||
|
wfHint_minimal: "Action directe : appeler, se coordonner, téléphoner et alerter",
|
||||||
wfNameMobile_activists: "Mouvement",
|
wfNameMobile_activists: "Mouvement",
|
||||||
wfModuleCount: "{n} modules",
|
wfModuleCount: "{n} modules",
|
||||||
activityFilter: "Filtrer",
|
activityFilter: "Filtrer",
|
||||||
|
|
@ -510,7 +516,9 @@ module.exports = {
|
||||||
wfHint_mobile: "Mugikorrerako oinarrizkoa",
|
wfHint_mobile: "Mugikorrerako oinarrizkoa",
|
||||||
workflow_acampada: "Kanpaldia",
|
workflow_acampada: "Kanpaldia",
|
||||||
workflow_activista: "Aktibista",
|
workflow_activista: "Aktibista",
|
||||||
wfHint_activista: "Ekintza zuzena: deitu, koordinatu, telefonoz hitz egin eta abisatu",
|
wfHint_activista: "Kolektibo baterako: Kanpaldiko guztia gehi kanpainak, alertak, inkestak, egutegia eta zereginak",
|
||||||
|
workflow_minimal: "Gutxienekoa",
|
||||||
|
wfHint_minimal: "Ekintza zuzena: deitu, koordinatu, telefonoz hitz egin eta abisatu",
|
||||||
wfNameMobile_activists: "Mugimendua",
|
wfNameMobile_activists: "Mugimendua",
|
||||||
wfModuleCount: "{n} modulu",
|
wfModuleCount: "{n} modulu",
|
||||||
activityFilter: "Iragazi",
|
activityFilter: "Iragazi",
|
||||||
|
|
@ -651,7 +659,9 @@ module.exports = {
|
||||||
wfHint_mobile: "Das Wichtigste fürs Handy",
|
wfHint_mobile: "Das Wichtigste fürs Handy",
|
||||||
workflow_acampada: "Camp",
|
workflow_acampada: "Camp",
|
||||||
workflow_activista: "Aktivist*in",
|
workflow_activista: "Aktivist*in",
|
||||||
wfHint_activista: "Direkte Aktion: aufrufen, koordinieren, telefonieren und warnen",
|
wfHint_activista: "Für ein Kollektiv: alles aus Camp plus Kampagnen, Warnungen, Umfragen, Kalender und Aufgaben",
|
||||||
|
workflow_minimal: "Minimal",
|
||||||
|
wfHint_minimal: "Direkte Aktion: aufrufen, koordinieren, telefonieren und warnen",
|
||||||
wfNameMobile_activists: "Bewegung",
|
wfNameMobile_activists: "Bewegung",
|
||||||
wfModuleCount: "{n} Module",
|
wfModuleCount: "{n} Module",
|
||||||
activityFilter: "Filtern",
|
activityFilter: "Filtern",
|
||||||
|
|
@ -792,7 +802,9 @@ module.exports = {
|
||||||
wfHint_mobile: "L'essenziale per il telefono",
|
wfHint_mobile: "L'essenziale per il telefono",
|
||||||
workflow_acampada: "Accampata",
|
workflow_acampada: "Accampata",
|
||||||
workflow_activista: "Attivista",
|
workflow_activista: "Attivista",
|
||||||
wfHint_activista: "Azione diretta: convocare, coordinarsi, chiamare e avvisare",
|
wfHint_activista: "Per un collettivo: tutto di Accampamento più campagne, allerte, sondaggi, calendario e compiti",
|
||||||
|
workflow_minimal: "Minimo",
|
||||||
|
wfHint_minimal: "Azione diretta: convocare, coordinarsi, chiamare e avvisare",
|
||||||
wfNameMobile_activists: "Movimento",
|
wfNameMobile_activists: "Movimento",
|
||||||
wfModuleCount: "{n} moduli",
|
wfModuleCount: "{n} moduli",
|
||||||
activityFilter: "Filtra",
|
activityFilter: "Filtra",
|
||||||
|
|
@ -933,7 +945,9 @@ module.exports = {
|
||||||
wfHint_mobile: "O básico para o telemóvel",
|
wfHint_mobile: "O básico para o telemóvel",
|
||||||
workflow_acampada: "Acampada",
|
workflow_acampada: "Acampada",
|
||||||
workflow_activista: "Ativista",
|
workflow_activista: "Ativista",
|
||||||
wfHint_activista: "Ação direta: convocar, coordenar, ligar e avisar",
|
wfHint_activista: "Para um coletivo: tudo do Acampamento mais campanhas, alertas, sondagens, calendário e tarefas",
|
||||||
|
workflow_minimal: "Mínimo",
|
||||||
|
wfHint_minimal: "Ação direta: convocar, coordenar, ligar e avisar",
|
||||||
wfNameMobile_activists: "Movimento",
|
wfNameMobile_activists: "Movimento",
|
||||||
wfModuleCount: "{n} módulos",
|
wfModuleCount: "{n} módulos",
|
||||||
activityFilter: "Filtrar",
|
activityFilter: "Filtrar",
|
||||||
|
|
@ -1074,7 +1088,9 @@ module.exports = {
|
||||||
wfHint_mobile: "手机基本功能",
|
wfHint_mobile: "手机基本功能",
|
||||||
workflow_acampada: "营地",
|
workflow_acampada: "营地",
|
||||||
workflow_activista: "活动者",
|
workflow_activista: "活动者",
|
||||||
wfHint_activista: "直接行动:召集、协调、通话和预警",
|
wfHint_activista: "适合团体:营地的全部功能,加上运动、预警、投票、日历和任务",
|
||||||
|
workflow_minimal: "极简",
|
||||||
|
wfHint_minimal: "直接行动:召集、协调、通话和预警",
|
||||||
wfNameMobile_activists: "运动",
|
wfNameMobile_activists: "运动",
|
||||||
wfModuleCount: "{n} 个模块",
|
wfModuleCount: "{n} 个模块",
|
||||||
activityFilter: "筛选",
|
activityFilter: "筛选",
|
||||||
|
|
@ -1215,7 +1231,9 @@ module.exports = {
|
||||||
wfHint_mobile: "الأساسيات للهاتف",
|
wfHint_mobile: "الأساسيات للهاتف",
|
||||||
workflow_acampada: "مخيم",
|
workflow_acampada: "مخيم",
|
||||||
workflow_activista: "ناشط",
|
workflow_activista: "ناشط",
|
||||||
wfHint_activista: "عمل مباشر: دعوة وتنسيق واتصال وتنبيه",
|
wfHint_activista: "لمجموعة: كل ما في المخيم مع الحملات والتنبيهات والاستطلاعات والتقويم والمهام",
|
||||||
|
workflow_minimal: "الأدنى",
|
||||||
|
wfHint_minimal: "عمل مباشر: دعوة وتنسيق واتصال وتنبيه",
|
||||||
wfNameMobile_activists: "الحركة",
|
wfNameMobile_activists: "الحركة",
|
||||||
wfModuleCount: "{n} وحدات",
|
wfModuleCount: "{n} وحدات",
|
||||||
activityFilter: "تصفية",
|
activityFilter: "تصفية",
|
||||||
|
|
@ -1356,7 +1374,9 @@ module.exports = {
|
||||||
wfHint_mobile: "फ़ोन के लिए बुनियादी",
|
wfHint_mobile: "फ़ोन के लिए बुनियादी",
|
||||||
workflow_acampada: "शिविर",
|
workflow_acampada: "शिविर",
|
||||||
workflow_activista: "कार्यकर्ता",
|
workflow_activista: "कार्यकर्ता",
|
||||||
wfHint_activista: "सीधी कार्रवाई: बुलाना, तालमेल, फ़ोन और चेतावनी",
|
wfHint_activista: "एक समूह के लिए: शिविर का सब कुछ, साथ में अभियान, चेतावनी, सर्वे, कैलेंडर और काम",
|
||||||
|
workflow_minimal: "न्यूनतम",
|
||||||
|
wfHint_minimal: "सीधी कार्रवाई: बुलाना, तालमेल, फ़ोन और चेतावनी",
|
||||||
wfNameMobile_activists: "आंदोलन",
|
wfNameMobile_activists: "आंदोलन",
|
||||||
wfModuleCount: "{n} मॉड्यूल",
|
wfModuleCount: "{n} मॉड्यूल",
|
||||||
activityFilter: "फ़िल्टर",
|
activityFilter: "फ़िल्टर",
|
||||||
|
|
@ -1497,7 +1517,9 @@ module.exports = {
|
||||||
wfHint_mobile: "Основное для телефона",
|
wfHint_mobile: "Основное для телефона",
|
||||||
workflow_acampada: "Лагерь",
|
workflow_acampada: "Лагерь",
|
||||||
workflow_activista: "Активист",
|
workflow_activista: "Активист",
|
||||||
wfHint_activista: "Прямое действие: созвать, координировать, звонить и предупреждать",
|
wfHint_activista: "Для коллектива: всё из Лагеря плюс кампании, тревоги, опросы, календарь и задачи",
|
||||||
|
workflow_minimal: "Минимум",
|
||||||
|
wfHint_minimal: "Прямое действие: созвать, координировать, звонить и предупреждать",
|
||||||
wfNameMobile_activists: "Движение",
|
wfNameMobile_activists: "Движение",
|
||||||
wfModuleCount: "{n} модулей",
|
wfModuleCount: "{n} модулей",
|
||||||
activityFilter: "Фильтр",
|
activityFilter: "Фильтр",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← شبكتي",
|
graphosBackToMine: "← شبكتي",
|
||||||
graphosShowing: "عرض",
|
graphosShowing: "عرض",
|
||||||
graphosYou: "أنت",
|
graphosYou: "أنت",
|
||||||
graphosTotalNodes: "إجمالي العقد",
|
graphosTotalNodes: "الإجمالي",
|
||||||
|
onlinePlural: "متصلون",
|
||||||
mentions: "الإشارات",
|
mentions: "الإشارات",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("كل رسالة تذكرك بـ @"),
|
strong("كل رسالة تذكرك بـ @"),
|
||||||
|
|
@ -420,6 +421,7 @@ module.exports = {
|
||||||
menuMedia: "المكتبة",
|
menuMedia: "المكتبة",
|
||||||
menuTools: "الأدوات",
|
menuTools: "الأدوات",
|
||||||
comment: "تعليق",
|
comment: "تعليق",
|
||||||
|
typeComment: "تعليق",
|
||||||
subtopic: "موضوع فرعي",
|
subtopic: "موضوع فرعي",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "إلغاء الدعم",
|
unfollow: "إلغاء الدعم",
|
||||||
|
|
@ -449,9 +451,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.",
|
"تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -730,10 +729,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"السؤال",
|
votesQuestionLabel:"السؤال",
|
||||||
voteStatusOpen:"مفتوح",
|
voteStatusOpen:"مفتوح",
|
||||||
voteStatusClosed:"مغلق",
|
voteStatusClosed:"مغلق",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" علّق على ",
|
|
||||||
a({ href: parentUrl }, " المحادثة"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`تعليق على منشور @${authorName}`],
|
commentTitle: ({ authorName }) => [`تعليق على منشور @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" أنشأ موضوعًا فرعيًا من ",
|
" أنشأ موضوعًا فرعيًا من ",
|
||||||
|
|
@ -868,6 +863,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "يدفع الـ PUB في دورته التالية وينشر تحويل دخل أساسي يمكنك تأكيده.",
|
bankRulesEligibility5: "يدفع الـ PUB في دورته التالية وينشر تحويل دخل أساسي يمكنك تأكيده.",
|
||||||
bankRulesEligibility6: "إذا غاب عنوانك أو كانت التغذية حديثة جداً أو رفضت الشهر أو لم يكن لدى الـ PUB أموال، تُتخطى المطالبة وتُسجَّل على الـ PUB.",
|
bankRulesEligibility6: "إذا غاب عنوانك أو كانت التغذية حديثة جداً أو رفضت الشهر أو لم يكن لدى الـ PUB أموال، تُتخطى المطالبة وتُسجَّل على الـ PUB.",
|
||||||
copyLinkTitle: "نسخ الرابط",
|
copyLinkTitle: "نسخ الرابط",
|
||||||
|
cardMenuTitle: "إجراءات أخرى",
|
||||||
|
shareContent: "مشاركة",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "رفض الدخل الأساسي",
|
bankRefuseUBI: "رفض الدخل الأساسي",
|
||||||
|
|
@ -901,7 +898,7 @@ module.exports = {
|
||||||
walletNotConfigured: "محفظة ECOin الخاصة بك غير مُهيأة بعد.",
|
walletNotConfigured: "محفظة ECOin الخاصة بك غير مُهيأة بعد.",
|
||||||
walletSettingsDescription: "دمج Oasis مع محفظة ECOin الخاصة بك.",
|
walletSettingsDescription: "دمج Oasis مع محفظة ECOin الخاصة بك.",
|
||||||
walletSettingsMobileDescription: "اربط Oasis بتطبيق ECOin Wallet على هذا الجهاز.",
|
walletSettingsMobileDescription: "اربط Oasis بتطبيق ECOin Wallet على هذا الجهاز.",
|
||||||
walletDownloadEcoin: "نزّل ECOin!",
|
walletDownloadEcoin: "نزّل ECOin",
|
||||||
walletRpcUrl: "عنوان RPC",
|
walletRpcUrl: "عنوان RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "مبلغ غير صالح",
|
invalid_amount: "مبلغ غير صالح",
|
||||||
|
|
@ -954,6 +951,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "تحديث",
|
bookmarkUpdateButton: "تحديث",
|
||||||
bookmarkAddFavoriteButton: "إضافة للمفضلة",
|
bookmarkAddFavoriteButton: "إضافة للمفضلة",
|
||||||
bookmarkRemoveFavoriteButton: "إزالة من المفضلة",
|
bookmarkRemoveFavoriteButton: "إزالة من المفضلة",
|
||||||
|
bookmarkTitleLabel: "العنوان",
|
||||||
bookmarkUrlLabel: "الرابط",
|
bookmarkUrlLabel: "الرابط",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "الوصف",
|
bookmarkDescriptionLabel: "الوصف",
|
||||||
|
|
@ -1098,9 +1096,9 @@ module.exports = {
|
||||||
allCVs: "السير الذاتية",
|
allCVs: "السير الذاتية",
|
||||||
discoverPeople: "اكتشف السكان في شبكتك.",
|
discoverPeople: "اكتشف السكان في شبكتك.",
|
||||||
searchInhabitantsPlaceholder: "ابحث في السكان...",
|
searchInhabitantsPlaceholder: "ابحث في السكان...",
|
||||||
filterLocation: "تصفية السكان حسب الموقع …",
|
filterLocation: "ابحث حسب الموقع…",
|
||||||
filterLanguage: "تصفية السكان حسب اللغة …",
|
filterLanguage: "ابحث حسب اللغة…",
|
||||||
filterSkills: "تصفية السكان حسب المهارات …",
|
filterSkills: "ابحث حسب المهارات…",
|
||||||
cvSkillsCount: "المهارات",
|
cvSkillsCount: "المهارات",
|
||||||
applyFilters: "تطبيق الفلاتر",
|
applyFilters: "تطبيق الفلاتر",
|
||||||
locationLabel: "الموقع",
|
locationLabel: "الموقع",
|
||||||
|
|
@ -1116,6 +1114,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "الأكثر نشاطًا",
|
topactivitySectionTitle: "الأكثر نشاطًا",
|
||||||
"TOP INACTIVITYButton": "الأقل نشاطاً",
|
"TOP INACTIVITYButton": "الأقل نشاطاً",
|
||||||
topinactivitySectionTitle: "السكان الأقل نشاطاً",
|
topinactivitySectionTitle: "السكان الأقل نشاطاً",
|
||||||
|
contactsSectionTitle: "جهات الاتصال",
|
||||||
blockedSectionTitle: "المحظورون",
|
blockedSectionTitle: "المحظورون",
|
||||||
gallerySectionTitle: "المعرض",
|
gallerySectionTitle: "المعرض",
|
||||||
topSectionTitle: "الأعلى",
|
topSectionTitle: "الأعلى",
|
||||||
|
|
@ -1569,9 +1568,7 @@ module.exports = {
|
||||||
voteUnknownDate: "تاريخ غير معروف",
|
voteUnknownDate: "تاريخ غير معروف",
|
||||||
voteDetailSectionTitle: 'تفاصيل التصويت',
|
voteDetailSectionTitle: 'تفاصيل التصويت',
|
||||||
voteCommentsLabel: 'التعليقات',
|
voteCommentsLabel: 'التعليقات',
|
||||||
voteCommentsForumButton: 'فتح النقاش',
|
voteNoCommentsYet: 'لا توجد تعليقات بعد.',
|
||||||
voteCommentsSectionTitle: 'فتح النقاش',
|
|
||||||
voteNoCommentsYet: 'لا توجد تعليقات بعد. كن أول من يرد.',
|
|
||||||
commentsGoToLast: "اذهب إلى آخر تعليق",
|
commentsGoToLast: "اذهب إلى آخر تعليق",
|
||||||
voteNewCommentPlaceholder: 'اكتب تعليقك هنا…',
|
voteNewCommentPlaceholder: 'اكتب تعليقك هنا…',
|
||||||
voteNewCommentButton: 'نشر التعليق',
|
voteNewCommentButton: 'نشر التعليق',
|
||||||
|
|
@ -1698,7 +1695,6 @@ module.exports = {
|
||||||
pollVoteButton: "صوّت",
|
pollVoteButton: "صوّت",
|
||||||
pollChangeVote: "تغيير الصوت",
|
pollChangeVote: "تغيير الصوت",
|
||||||
pollVoters: "المصوّتون",
|
pollVoters: "المصوّتون",
|
||||||
pollComments: "التعليقات",
|
|
||||||
pollStatusLabel: "الحالة",
|
pollStatusLabel: "الحالة",
|
||||||
pollStatusOpen: "مفتوح",
|
pollStatusOpen: "مفتوح",
|
||||||
pollStatusClosed: "مغلق",
|
pollStatusClosed: "مغلق",
|
||||||
|
|
@ -1720,9 +1716,22 @@ module.exports = {
|
||||||
blogAllowComments: "السماح بالتعليقات",
|
blogAllowComments: "السماح بالتعليقات",
|
||||||
blogCommentsClosed: "أغلق الكاتب التعليقات على هذه المدونة.",
|
blogCommentsClosed: "أغلق الكاتب التعليقات على هذه المدونة.",
|
||||||
blogNoItems: "لم يتم العثور على مدونات.",
|
blogNoItems: "لم يتم العثور على مدونات.",
|
||||||
viewBlog: "عرض المدونة",
|
|
||||||
forumTitle: "المنتديات",
|
forumTitle: "المنتديات",
|
||||||
forumCategoryLabel: "الفئة",
|
forumCategoryLabel: "الفئة",
|
||||||
|
forumCatGENERAL: "عام",
|
||||||
|
forumCatPOLITICS: "سياسة",
|
||||||
|
forumCatTECH: "تقنية",
|
||||||
|
forumCatSCIENCE: "علوم",
|
||||||
|
forumCatMUSIC: "موسيقى",
|
||||||
|
forumCatART: "فن",
|
||||||
|
forumCatGAMING: "ألعاب",
|
||||||
|
forumCatBOOKS: "كتب",
|
||||||
|
forumCatFILMS: "أفلام",
|
||||||
|
forumCatPHILOSOPHY: "فلسفة",
|
||||||
|
forumCatSOCIETY: "مجتمع",
|
||||||
|
forumCatPRIVACY: "خصوصية",
|
||||||
|
forumCatCYBERWARFARE: "حرب سيبرانية",
|
||||||
|
forumCatSURVIVALISM: "البقاء",
|
||||||
forumTitleLabel: "العنوان",
|
forumTitleLabel: "العنوان",
|
||||||
forumTitlePlaceholder: "عنوان المنتدى...",
|
forumTitlePlaceholder: "عنوان المنتدى...",
|
||||||
forumCreateButton: "إنشاء منتدى",
|
forumCreateButton: "إنشاء منتدى",
|
||||||
|
|
@ -1991,43 +2000,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "التقارير قيد المراجعة",
|
reportsUnderReviewSectionTitle: "التقارير قيد المراجعة",
|
||||||
reportsResolvedSectionTitle: "التقارير المحلولة",
|
reportsResolvedSectionTitle: "التقارير المحلولة",
|
||||||
reportsInvalidSectionTitle: "التقارير غير الصالحة",
|
reportsInvalidSectionTitle: "التقارير غير الصالحة",
|
||||||
reportsBugTemplateTitle: 'تفاصيل إعادة الإنتاج (الأخطاء)',
|
|
||||||
reportsFeatureTemplateTitle: 'التفاصيل (الميزات)',
|
|
||||||
reportsAbuseTemplateTitle: 'التفاصيل (الإساءة)',
|
|
||||||
reportsContentTemplateTitle: 'التفاصيل (مشاكل المحتوى)',
|
|
||||||
reportsStepsToReproduceLabel: 'خطوات إعادة الإنتاج',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'النتيجة المتوقعة',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'ماذا يجب أن يحدث؟',
|
|
||||||
reportsActualBehaviorLabel: 'النتيجة الفعلية',
|
|
||||||
reportsActualBehaviorPlaceholder: 'ماذا يحدث فعلاً؟',
|
|
||||||
reportsEnvironmentLabel: 'البيئة',
|
|
||||||
reportsEnvironmentPlaceholder: 'الإصدار، الجهاز، نظام التشغيل، المتصفح، الإعدادات، السجلات، إلخ.',
|
|
||||||
reportsReproduceRateLabel: 'معدل إعادة الإنتاج',
|
|
||||||
reportsReproduceRateAlways: 'دائمًا',
|
|
||||||
reportsReproduceRateOften: 'غالبًا',
|
|
||||||
reportsReproduceRateSometimes: 'أحيانًا',
|
|
||||||
reportsReproduceRateRarely: 'نادرًا',
|
|
||||||
reportsReproduceRateUnable: 'غير قابل لإعادة الإنتاج',
|
|
||||||
reportsReproduceRateUnknown: 'غير محدد',
|
|
||||||
reportsProblemStatementLabel: 'المشكلة / الحاجة',
|
|
||||||
reportsProblemStatementPlaceholder: 'ما المشكلة التي تحلها هذه الميزة؟',
|
|
||||||
reportsUserStoryLabel: 'قصة الساكن',
|
|
||||||
reportsUserStoryPlaceholder: 'بصفتي <ساكن>، أريد <إجراء>، لكي <فائدة>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'معايير القبول',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- بفرض...\n- عندما...\n- عندها...',
|
|
||||||
reportsWhatHappenedLabel: 'ماذا حدث؟',
|
|
||||||
reportsWhatHappenedPlaceholder: 'صف الحادثة مع السياق والتواريخ التقريبية.',
|
|
||||||
reportsReportedUserLabel: 'الساكن / الكيان المُبلَّغ عنه',
|
|
||||||
reportsReportedUserPlaceholder: '@ساكن، تغذية، معرّف، رابط، إلخ.',
|
|
||||||
reportsEvidenceLinksLabel: 'الأدلة / الروابط',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'الصق الروابط، معرّفات الرسائل، لقطات الشاشة (إن وُجدت)، إلخ.',
|
|
||||||
reportsContentLocationLabel: 'مكان المحتوى',
|
|
||||||
reportsContentLocationPlaceholder: 'رابط، معرّف، قناة، محادثة، مؤلف، إلخ.',
|
|
||||||
reportsWhyInappropriateLabel: "لماذا هو غير مناسب",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'اشرح السبب والتأثير.',
|
|
||||||
reportsRequestedActionLabel: 'الإجراء المطلوب',
|
|
||||||
reportsRequestedActionPlaceholder: 'إزالة، إخفاء، وسم، تحذير، إلخ.',
|
|
||||||
tribesTitle: "القبائل",
|
tribesTitle: "القبائل",
|
||||||
tribeFilterAll: "الكل",
|
tribeFilterAll: "الكل",
|
||||||
tribeFilterRecent: "الأحدث",
|
tribeFilterRecent: "الأحدث",
|
||||||
|
|
@ -2271,6 +2243,7 @@ module.exports = {
|
||||||
voteWellResearched: "بحث جيد",
|
voteWellResearched: "بحث جيد",
|
||||||
voteNeedsSources: "يحتاج مصادر",
|
voteNeedsSources: "يحتاج مصادر",
|
||||||
voteWrong: "خاطئ",
|
voteWrong: "خاطئ",
|
||||||
|
voteOutdated: "قديم",
|
||||||
voteLowQuality: "جودة منخفضة",
|
voteLowQuality: "جودة منخفضة",
|
||||||
voteLove: "حب",
|
voteLove: "حب",
|
||||||
voteClear: "واضح",
|
voteClear: "واضح",
|
||||||
|
|
@ -2515,7 +2488,6 @@ module.exports = {
|
||||||
shopNoProducts: "لا توجد منتجات بعد.",
|
shopNoProducts: "لا توجد منتجات بعد.",
|
||||||
shopOutOfStock: "نفد المخزون",
|
shopOutOfStock: "نفد المخزون",
|
||||||
shopBuy: "شراء",
|
shopBuy: "شراء",
|
||||||
shopBuyEncryptedNote: "بيانات التوصيل الخاصة بك مشفّرة ولا يراها سوى صاحب المتجر.",
|
|
||||||
shopBuyDeliveryAddress: "عنوان التوصيل",
|
shopBuyDeliveryAddress: "عنوان التوصيل",
|
||||||
shopBuyContact: "جهة الاتصال",
|
shopBuyContact: "جهة الاتصال",
|
||||||
shopBuyContactPlaceholder: "البريد، الهاتف، إلخ.",
|
shopBuyContactPlaceholder: "البريد، الهاتف، إلخ.",
|
||||||
|
|
@ -3032,6 +3004,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "الوصف",
|
industryDescriptionLabel: "الوصف",
|
||||||
industryDescriptionPlaceholder: "ماذا ينتج هذا المصنع؟",
|
industryDescriptionPlaceholder: "ماذا ينتج هذا المصنع؟",
|
||||||
industrySector: "القطاع",
|
industrySector: "القطاع",
|
||||||
|
industrySector_software: "برمجيات",
|
||||||
|
industrySector_hardware: "عتاد",
|
||||||
|
industrySector_agriculture: "زراعة",
|
||||||
|
industrySector_textile: "نسيج",
|
||||||
|
industrySector_energy: "طاقة",
|
||||||
|
industrySector_food: "غذاء",
|
||||||
|
industrySector_construction: "بناء",
|
||||||
|
industrySector_media: "إعلام",
|
||||||
|
industrySector_services: "خدمات",
|
||||||
|
industrySector_other: "أخرى",
|
||||||
industryMembershipPolicy: "سياسة العضوية",
|
industryMembershipPolicy: "سياسة العضوية",
|
||||||
industryQuorum: "النصاب",
|
industryQuorum: "النصاب",
|
||||||
industryMajority: "الأغلبية",
|
industryMajority: "الأغلبية",
|
||||||
|
|
@ -3233,6 +3215,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "وحدة لإدارة المحتوى المفضل لديك.",
|
modulesFavoritesDescription: "وحدة لإدارة المحتوى المفضل لديك.",
|
||||||
fileTooLargeTitle: "الملف كبير جدًا",
|
fileTooLargeTitle: "الملف كبير جدًا",
|
||||||
fileTooLargeMessage: "يتجاوز الملف الحجم الأقصى المسموح به (50 ميغابايت). يرجى اختيار ملف أصغر.",
|
fileTooLargeMessage: "يتجاوز الملف الحجم الأقصى المسموح به (50 ميغابايت). يرجى اختيار ملف أصغر.",
|
||||||
|
fileWrongType: "هذا الملف ليس من النوع المناسب لهذا القسم.",
|
||||||
|
mediaLockedAfterOpinions: "لديه آراء بالفعل: يمكنك تعديل الباقي، لكن لا يمكنك تغيير ما تم تقييمه.",
|
||||||
|
pdfPrevPage: "الصفحة السابقة",
|
||||||
|
pdfNextPage: "الصفحة التالية",
|
||||||
|
pdfPage: "صفحة",
|
||||||
|
pdfZoomIn: "تكبير",
|
||||||
|
pdfZoomOut: "تصغير",
|
||||||
|
pdfFullscreen: "ملء الشاشة",
|
||||||
|
pdfClose: "إغلاق",
|
||||||
|
pdfRotate: "تدوير",
|
||||||
|
contentNotAvailableYet: "هذا المحتوى غير متاح بعد.",
|
||||||
|
pdfRetry: "إعادة المحاولة",
|
||||||
goBack: "رجوع",
|
goBack: "رجوع",
|
||||||
errorDismiss: "حسنًا",
|
errorDismiss: "حسنًا",
|
||||||
errorPageTitle: "حدث خطأ",
|
errorPageTitle: "حدث خطأ",
|
||||||
|
|
@ -3275,9 +3269,7 @@ module.exports = {
|
||||||
uploadMedia: "رفع وسائط (الحد الأقصى: 50 ميغابايت)",
|
uploadMedia: "رفع وسائط (الحد الأقصى: 50 ميغابايت)",
|
||||||
feedOpenDiscussion: "فتح النقاش",
|
feedOpenDiscussion: "فتح النقاش",
|
||||||
feedDetailTitle: "التغذية",
|
feedDetailTitle: "التغذية",
|
||||||
feedPostComment: "نشر تعليق",
|
|
||||||
courtsRespondentInvalid: "يجب أن يكون معرّف SSB صالحًا (@...ed25519)",
|
courtsRespondentInvalid: "يجب أن يكون معرّف SSB صالحًا (@...ed25519)",
|
||||||
noComments: "لا توجد تعليقات بعد",
|
|
||||||
mapsLabel: "خرائط",
|
mapsLabel: "خرائط",
|
||||||
mapTitle: "خرائط",
|
mapTitle: "خرائط",
|
||||||
mapDescription: "أدر الخرائط غير المتصلة وشاركها في شبكتك.",
|
mapDescription: "أدر الخرائط غير المتصلة وشاركها في شبكتك.",
|
||||||
|
|
@ -3581,6 +3573,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل",
|
podcastTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل",
|
||||||
podcastFilterAll: "الكل",
|
podcastFilterAll: "الكل",
|
||||||
podcastFilterMine: "قنواتي",
|
podcastFilterMine: "قنواتي",
|
||||||
|
podcastFilterFavorites: "المفضلة",
|
||||||
|
podcastEpisodeNumberLabel: "رقم الحلقة",
|
||||||
podcastFilterRecent: "الأحدث",
|
podcastFilterRecent: "الأحدث",
|
||||||
podcastFilterTop: "الأفضل",
|
podcastFilterTop: "الأفضل",
|
||||||
podcastFilterViewers: "المستمعون",
|
podcastFilterViewers: "المستمعون",
|
||||||
|
|
@ -3799,7 +3793,6 @@ module.exports = {
|
||||||
wikiBackToPage: "العودة إلى الصفحة",
|
wikiBackToPage: "العودة إلى الصفحة",
|
||||||
wikiLinkedChip: "مرتبطة",
|
wikiLinkedChip: "مرتبطة",
|
||||||
wikiRestoredSummary: "إصدار مستعاد",
|
wikiRestoredSummary: "إصدار مستعاد",
|
||||||
wikiExportPdf: "إنشاء الصفحة",
|
|
||||||
wikiPreview: "معاينة",
|
wikiPreview: "معاينة",
|
||||||
tribeSectionWiki: "ويكي",
|
tribeSectionWiki: "ويكي",
|
||||||
tribeWikiCreate: "إنشاء ويكي",
|
tribeWikiCreate: "إنشاء ويكي",
|
||||||
|
|
@ -3867,6 +3860,9 @@ module.exports = {
|
||||||
roomRecStop: "إيقاف التسجيل",
|
roomRecStop: "إيقاف التسجيل",
|
||||||
roomStreamFile: "بثّ",
|
roomStreamFile: "بثّ",
|
||||||
roomStreamStop: "إيقاف",
|
roomStreamStop: "إيقاف",
|
||||||
|
roomScreen: "الشاشة",
|
||||||
|
roomScreenSharing: "مشاركة الشاشة",
|
||||||
|
roomScreenWarning: "سيرى كل من في الغرفة شاشتك بالكامل حتى تتوقف.",
|
||||||
roomStreaming: "جارٍ البث",
|
roomStreaming: "جارٍ البث",
|
||||||
liveFullscreen: "ملء الشاشة",
|
liveFullscreen: "ملء الشاشة",
|
||||||
roomStreamPickTitle: "اختر فيديو",
|
roomStreamPickTitle: "اختر فيديو",
|
||||||
|
|
@ -3924,6 +3920,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "أنت منضم بالفعل. يمكنك فتحه من وحدته.",
|
inviteAlreadyMember: "أنت منضم بالفعل. يمكنك فتحه من وحدته.",
|
||||||
actionFailed: "تعذّر تنفيذ ذلك. حاول مرة أخرى.",
|
actionFailed: "تعذّر تنفيذ ذلك. حاول مرة أخرى.",
|
||||||
publishTooShort: "منشورك قصير جدًا.",
|
publishTooShort: "منشورك قصير جدًا.",
|
||||||
|
publishEmpty: "اكتب شيئًا أو أرفق ملفًا.",
|
||||||
walletSendFailed: "تعذّر إرسال الدفعة: {reason}",
|
walletSendFailed: "تعذّر إرسال الدفعة: {reason}",
|
||||||
pixeliaCoordsError: "هذه الإحداثيات خارج الشبكة.",
|
pixeliaCoordsError: "هذه الإحداثيات خارج الشبكة.",
|
||||||
courtsErrTitle: "العنوان مطلوب.",
|
courtsErrTitle: "العنوان مطلوب.",
|
||||||
|
|
@ -4435,6 +4432,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "الوسوم",
|
torrentTagsLabel: "الوسوم",
|
||||||
torrentTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل",
|
torrentTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل",
|
||||||
torrentSizeLabel: "الحجم",
|
torrentSizeLabel: "الحجم",
|
||||||
|
torrentInfoHash: "بصمة المعلومات",
|
||||||
|
torrentFilesLabel: "الملفات",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "إنشاء تورنت",
|
torrentFromContent: "إنشاء تورنت",
|
||||||
torrentFromContentMissing: "الملف غير متاح بعد على هذه العقدة. حاولي لاحقًا.",
|
torrentFromContentMissing: "الملف غير متاح بعد على هذه العقدة. حاولي لاحقًا.",
|
||||||
torrentDownloads: "التنزيلات",
|
torrentDownloads: "التنزيلات",
|
||||||
|
|
@ -4520,9 +4520,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "قبول",
|
inhabitantsPendingAccept: "قبول",
|
||||||
inhabitantsPendingReject: "رفض",
|
inhabitantsPendingReject: "رفض",
|
||||||
tribeSectionGovernance: "الحوكمة",
|
tribeSectionGovernance: "الحوكمة",
|
||||||
tribePadInviteRequired: "لا يمكنك الوصول. اطلب دعوة.",
|
|
||||||
tribeChatInviteRequired: "لا يمكنك الوصول للدردشة. اطلب دعوة.",
|
|
||||||
tribeGovernanceDesc: "الحوكمة الداخلية لهذه القبيلة.",
|
|
||||||
tribeGovernanceNoGov: "لا توجد حكومة نشطة",
|
tribeGovernanceNoGov: "لا توجد حكومة نشطة",
|
||||||
tribeGovernanceNoGovDesc: "لم تنتخب هذه القبيلة حكومة بعد.",
|
tribeGovernanceNoGovDesc: "لم تنتخب هذه القبيلة حكومة بعد.",
|
||||||
tribeGovCardTitle: "الحكومة الحالية",
|
tribeGovCardTitle: "الحكومة الحالية",
|
||||||
|
|
@ -4579,7 +4576,6 @@ module.exports = {
|
||||||
logsFilterMonth: "الشهر الماضي",
|
logsFilterMonth: "الشهر الماضي",
|
||||||
logsFilterWeek: "الأسبوع الماضي",
|
logsFilterWeek: "الأسبوع الماضي",
|
||||||
logsFilterToday: "اليوم",
|
logsFilterToday: "اليوم",
|
||||||
logsComments: "التعليقات",
|
|
||||||
logsAuthorEntries: "المدخلات",
|
logsAuthorEntries: "المدخلات",
|
||||||
logsCreate: "إنشاء إدخال",
|
logsCreate: "إنشاء إدخال",
|
||||||
logsExport: "تصدير السجل",
|
logsExport: "تصدير السجل",
|
||||||
|
|
@ -4735,6 +4731,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "اختبار WILL",
|
larpWillTestTitle: "اختبار WILL",
|
||||||
settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية.",
|
settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية.",
|
||||||
settingsLanEnable: "تفعيل بث الشبكة المحلية",
|
settingsLanEnable: "تفعيل بث الشبكة المحلية",
|
||||||
|
settingsConnectionsTitle: "الاتصالات",
|
||||||
|
settingsConnectionsDesc: "عدد النظراء الذين تبقيهم هذه العقدة متصلين في آن واحد.",
|
||||||
settingsLanTitle: "بث الشبكة المحلية",
|
settingsLanTitle: "بث الشبكة المحلية",
|
||||||
settingsReplicationTitle: "النسخ",
|
settingsReplicationTitle: "النسخ",
|
||||||
settingsReplicationDesc: "اضبط عدد القفزات التي يتبعها نظيرك انطلاقًا من خلاصتك عند نسخ المحتوى.",
|
settingsReplicationDesc: "اضبط عدد القفزات التي يتبعها نظيرك انطلاقًا من خلاصتك عند نسخ المحتوى.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← La meva xarxa",
|
graphosBackToMine: "← La meva xarxa",
|
||||||
graphosShowing: "Mostrant",
|
graphosShowing: "Mostrant",
|
||||||
graphosYou: "Tu",
|
graphosYou: "Tu",
|
||||||
graphosTotalNodes: "Nodes totals",
|
graphosTotalNodes: "Totals",
|
||||||
|
onlinePlural: "Connectats",
|
||||||
mentions: "Mencions",
|
mentions: "Mencions",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Tots els missatges que et @mencionen"),
|
strong("Tots els missatges que et @mencionen"),
|
||||||
|
|
@ -412,6 +413,7 @@ module.exports = {
|
||||||
menuMedia: "Biblioteca",
|
menuMedia: "Biblioteca",
|
||||||
menuTools: "Eines",
|
menuTools: "Eines",
|
||||||
comment: "Comentar",
|
comment: "Comentar",
|
||||||
|
typeComment: "Comentari",
|
||||||
subtopic: "Subtema",
|
subtopic: "Subtema",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Deixar de donar suport",
|
unfollow: "Deixar de donar suport",
|
||||||
|
|
@ -441,9 +443,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!",
|
"RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -718,10 +717,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Pregunta",
|
votesQuestionLabel:"Pregunta",
|
||||||
voteStatusOpen:"OBERT",
|
voteStatusOpen:"OBERT",
|
||||||
voteStatusClosed:"TANCAT",
|
voteStatusClosed:"TANCAT",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" ha comentat en ",
|
|
||||||
a({ href: parentUrl }, " fil"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Ha comentat al post de @${authorName}`],
|
commentTitle: ({ authorName }) => [`Ha comentat al post de @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" ha creat un subtema des de ",
|
" ha creat un subtema des de ",
|
||||||
|
|
@ -856,6 +851,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "El PUB paga en el seu següent cicle i publica un transfer RBU que pots confirmar.",
|
bankRulesEligibility5: "El PUB paga en el seu següent cicle i publica un transfer RBU que pots confirmar.",
|
||||||
bankRulesEligibility6: "Si falta la teva adreça, el feed és massa jove, has rebutjat el mes o el PUB no té fons, la reclamació se salta i queda registrada al PUB.",
|
bankRulesEligibility6: "Si falta la teva adreça, el feed és massa jove, has rebutjat el mes o el PUB no té fons, la reclamació se salta i queda registrada al PUB.",
|
||||||
copyLinkTitle: "Copiar l'enllaç",
|
copyLinkTitle: "Copiar l'enllaç",
|
||||||
|
cardMenuTitle: "Més accions",
|
||||||
|
shareContent: "Compartir",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Rebutjar RBU",
|
bankRefuseUBI: "Rebutjar RBU",
|
||||||
|
|
@ -889,7 +886,7 @@ module.exports = {
|
||||||
walletNotConfigured: "La teva cartera d'ECOin encara no està configurada.",
|
walletNotConfigured: "La teva cartera d'ECOin encara no està configurada.",
|
||||||
walletSettingsDescription: "Integrar Oasis amb el teu moneder d'ECOin.",
|
walletSettingsDescription: "Integrar Oasis amb el teu moneder d'ECOin.",
|
||||||
walletSettingsMobileDescription: "Vincula Oasis amb l'app ECOin Wallet d'aquest dispositiu.",
|
walletSettingsMobileDescription: "Vincula Oasis amb l'app ECOin Wallet d'aquest dispositiu.",
|
||||||
walletDownloadEcoin: "Baixar ECOin!",
|
walletDownloadEcoin: "Baixar ECOin",
|
||||||
walletRpcUrl: "URL del RPC",
|
walletRpcUrl: "URL del RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Quantitat no vàlida",
|
invalid_amount: "Quantitat no vàlida",
|
||||||
|
|
@ -942,6 +939,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Actualitzar",
|
bookmarkUpdateButton: "Actualitzar",
|
||||||
bookmarkAddFavoriteButton: "Afegir a preferits",
|
bookmarkAddFavoriteButton: "Afegir a preferits",
|
||||||
bookmarkRemoveFavoriteButton: "Treure de preferits",
|
bookmarkRemoveFavoriteButton: "Treure de preferits",
|
||||||
|
bookmarkTitleLabel: "Títol",
|
||||||
bookmarkUrlLabel: "Enllaç",
|
bookmarkUrlLabel: "Enllaç",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Descripció",
|
bookmarkDescriptionLabel: "Descripció",
|
||||||
|
|
@ -1086,9 +1084,9 @@ module.exports = {
|
||||||
allCVs: "CV",
|
allCVs: "CV",
|
||||||
discoverPeople: "Descobreix habitants a la teva xarxa.",
|
discoverPeople: "Descobreix habitants a la teva xarxa.",
|
||||||
searchInhabitantsPlaceholder: "Cercar a Habitants...",
|
searchInhabitantsPlaceholder: "Cercar a Habitants...",
|
||||||
filterLocation: "FILTRAR habitants PER UBICACIÓ …",
|
filterLocation: "Cercar per ubicació…",
|
||||||
filterLanguage: "FILTRAR habitants PER IDIOMA …",
|
filterLanguage: "Cercar per idioma…",
|
||||||
filterSkills: "FILTRAR habitants PER HABILITATS …",
|
filterSkills: "Cercar per habilitats…",
|
||||||
cvSkillsCount: "Habilitats",
|
cvSkillsCount: "Habilitats",
|
||||||
applyFilters: "Aplicar els filtres",
|
applyFilters: "Aplicar els filtres",
|
||||||
locationLabel: "Ubicació",
|
locationLabel: "Ubicació",
|
||||||
|
|
@ -1104,6 +1102,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Més activitat",
|
topactivitySectionTitle: "Més activitat",
|
||||||
"TOP INACTIVITYButton": "MÉS INACTIUS",
|
"TOP INACTIVITYButton": "MÉS INACTIUS",
|
||||||
topinactivitySectionTitle: "Habitants menys actius",
|
topinactivitySectionTitle: "Habitants menys actius",
|
||||||
|
contactsSectionTitle: "Contactes",
|
||||||
blockedSectionTitle: "Bloquejats",
|
blockedSectionTitle: "Bloquejats",
|
||||||
gallerySectionTitle: "GALERIA",
|
gallerySectionTitle: "GALERIA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1558,9 +1557,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Data desconeguda",
|
voteUnknownDate: "Data desconeguda",
|
||||||
voteDetailSectionTitle: "Detalls de la votació",
|
voteDetailSectionTitle: "Detalls de la votació",
|
||||||
voteCommentsLabel: "Comentaris",
|
voteCommentsLabel: "Comentaris",
|
||||||
voteCommentsForumButton: "Obrir la conversa",
|
voteNoCommentsYet: "Encara no hi ha comentaris.",
|
||||||
voteCommentsSectionTitle: "Conversa oberta",
|
|
||||||
voteNoCommentsYet: "Encara no hi ha comentaris. Sigues la primera persona a opinar.",
|
|
||||||
commentsGoToLast: "Anar al darrer comentari",
|
commentsGoToLast: "Anar al darrer comentari",
|
||||||
voteNewCommentPlaceholder: "Escriu aquí el teu comentari…",
|
voteNewCommentPlaceholder: "Escriu aquí el teu comentari…",
|
||||||
voteNewCommentButton: "Publicar el comentari",
|
voteNewCommentButton: "Publicar el comentari",
|
||||||
|
|
@ -1687,7 +1684,6 @@ module.exports = {
|
||||||
pollVoteButton: "Votar",
|
pollVoteButton: "Votar",
|
||||||
pollChangeVote: "Canviar el vot",
|
pollChangeVote: "Canviar el vot",
|
||||||
pollVoters: "Votants",
|
pollVoters: "Votants",
|
||||||
pollComments: "Comentaris",
|
|
||||||
pollStatusLabel: "Estat",
|
pollStatusLabel: "Estat",
|
||||||
pollStatusOpen: "OBERTA",
|
pollStatusOpen: "OBERTA",
|
||||||
pollStatusClosed: "TANCADA",
|
pollStatusClosed: "TANCADA",
|
||||||
|
|
@ -1709,9 +1705,22 @@ module.exports = {
|
||||||
blogAllowComments: "Permetre comentaris",
|
blogAllowComments: "Permetre comentaris",
|
||||||
blogCommentsClosed: "L'autor ha tancat els comentaris d'aquest blog.",
|
blogCommentsClosed: "L'autor ha tancat els comentaris d'aquest blog.",
|
||||||
blogNoItems: "No s'han trobat blogs.",
|
blogNoItems: "No s'han trobat blogs.",
|
||||||
viewBlog: "Veure el blog",
|
|
||||||
forumTitle: "Fòrums",
|
forumTitle: "Fòrums",
|
||||||
forumCategoryLabel: "Categoria",
|
forumCategoryLabel: "Categoria",
|
||||||
|
forumCatGENERAL: "GENERAL",
|
||||||
|
forumCatPOLITICS: "POLÍTICA",
|
||||||
|
forumCatTECH: "TECNOLOGIA",
|
||||||
|
forumCatSCIENCE: "CIÈNCIA",
|
||||||
|
forumCatMUSIC: "MÚSICA",
|
||||||
|
forumCatART: "ART",
|
||||||
|
forumCatGAMING: "JOCS",
|
||||||
|
forumCatBOOKS: "LLIBRES",
|
||||||
|
forumCatFILMS: "PEL·LÍCULES",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFIA",
|
||||||
|
forumCatSOCIETY: "SOCIETAT",
|
||||||
|
forumCatPRIVACY: "PRIVADESA",
|
||||||
|
forumCatCYBERWARFARE: "CIBERGUERRA",
|
||||||
|
forumCatSURVIVALISM: "SUPERVIVÈNCIA",
|
||||||
forumTitleLabel: "Títol",
|
forumTitleLabel: "Títol",
|
||||||
forumTitlePlaceholder: "Títol del fòrum...",
|
forumTitlePlaceholder: "Títol del fòrum...",
|
||||||
forumCreateButton: "Crear un fòrum",
|
forumCreateButton: "Crear un fòrum",
|
||||||
|
|
@ -1770,8 +1779,6 @@ module.exports = {
|
||||||
feedTitle: "Feed",
|
feedTitle: "Feed",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Obrir el debat",
|
feedOpenDiscussion: "Obrir el debat",
|
||||||
feedPostComment: "Publicar el comentari",
|
|
||||||
noComments: "Encara no hi ha comentaris",
|
|
||||||
createFeedTitle: "Crear un feed",
|
createFeedTitle: "Crear un feed",
|
||||||
createFeedButton: "Enviar el feed!",
|
createFeedButton: "Enviar el feed!",
|
||||||
feedPlaceholder: "Què està passant? (màxim 280 caràcters)",
|
feedPlaceholder: "Què està passant? (màxim 280 caràcters)",
|
||||||
|
|
@ -1982,42 +1989,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Informes en revisió",
|
reportsUnderReviewSectionTitle: "Informes en revisió",
|
||||||
reportsResolvedSectionTitle: "Informes resolts",
|
reportsResolvedSectionTitle: "Informes resolts",
|
||||||
reportsInvalidSectionTitle: "Informes no vàlids",
|
reportsInvalidSectionTitle: "Informes no vàlids",
|
||||||
reportsBugTemplateTitle: "Dades per reproduir (errors)",
|
|
||||||
reportsFeatureTemplateTitle: "Detalls (funcions)",
|
|
||||||
reportsAbuseTemplateTitle: "Detalls (abús)",
|
|
||||||
reportsContentTemplateTitle: "Detalls (problemes de contingut)",
|
|
||||||
reportsStepsToReproduceLabel: "Passos per reproduir-ho",
|
|
||||||
reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...",
|
|
||||||
reportsExpectedBehaviorLabel: "Resultat esperat",
|
|
||||||
reportsExpectedBehaviorPlaceholder: "Què hauria de passar?",
|
|
||||||
reportsActualBehaviorLabel: "Resultat real",
|
|
||||||
reportsActualBehaviorPlaceholder: "Què passa realment?",
|
|
||||||
reportsEnvironmentLabel: "Entorn",
|
|
||||||
reportsEnvironmentPlaceholder: "Versió, dispositiu, SO, navegador, configuració, registres, etc.",
|
|
||||||
reportsReproduceRateLabel: "Freqüència",
|
|
||||||
reportsReproduceRateAlways: "Sempre",
|
|
||||||
reportsReproduceRateOften: "Sovint",
|
|
||||||
reportsReproduceRateSometimes: "De vegades",
|
|
||||||
reportsReproduceRateRarely: "Rarament",
|
|
||||||
reportsReproduceRateUnable: "No es pot reproduir",
|
|
||||||
reportsReproduceRateUnknown: "No especificat",
|
|
||||||
reportsProblemStatementLabel: "Problema / necessitat",
|
|
||||||
reportsProblemStatementPlaceholder: "Quin problema resol aquesta funció?",
|
|
||||||
reportsUserStoryLabel: "Història d'habitant",
|
|
||||||
reportsUserStoryPlaceholder: "Com a <habitant>, vull <acció>, per <benefici>.",
|
|
||||||
reportsAcceptanceCriteriaLabel: "Criteris d'acceptació",
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: "- Donat...\n- Quan...\n- Aleshores...",
|
|
||||||
reportsWhatHappenedLabel: "Què ha passat?",
|
|
||||||
reportsWhatHappenedPlaceholder: "Descriu l'incident amb context i dates aproximades.",
|
|
||||||
reportsReportedUserLabel: "Habitant / entitat denunciada",
|
|
||||||
reportsReportedUserPlaceholder: "@habitant, feed, ID, enllaç, etc.",
|
|
||||||
reportsEvidenceLinksLabel: "Proves / enllaços",
|
|
||||||
reportsEvidenceLinksPlaceholder: "Enganxa enllaços, IDs de missatges, captures (si escau), etc.",
|
|
||||||
reportsContentLocationLabel: "On és el contingut",
|
|
||||||
reportsContentLocationPlaceholder: "Enllaç, ID, canal, fil, autor, etc.",
|
|
||||||
reportsWhyInappropriatePlaceholder: "Explica el motiu i l'impacte.",
|
|
||||||
reportsRequestedActionLabel: "Acció sol·licitada",
|
|
||||||
reportsRequestedActionPlaceholder: "Eliminar, amagar, marcar, advertir, etc.",
|
|
||||||
tribesTitle: "Tribus",
|
tribesTitle: "Tribus",
|
||||||
tribeFilterAll: "TOTES",
|
tribeFilterAll: "TOTES",
|
||||||
tribeFilterRecent: "RECENTS",
|
tribeFilterRecent: "RECENTS",
|
||||||
|
|
@ -2261,6 +2232,7 @@ module.exports = {
|
||||||
voteWellResearched: "Ben documentat",
|
voteWellResearched: "Ben documentat",
|
||||||
voteNeedsSources: "Necessita fonts",
|
voteNeedsSources: "Necessita fonts",
|
||||||
voteWrong: "Incorrecte",
|
voteWrong: "Incorrecte",
|
||||||
|
voteOutdated: "Desactualitzat",
|
||||||
voteLowQuality: "Baixa qualitat",
|
voteLowQuality: "Baixa qualitat",
|
||||||
voteLove: "Amor",
|
voteLove: "Amor",
|
||||||
voteClear: "Clar",
|
voteClear: "Clar",
|
||||||
|
|
@ -2505,7 +2477,6 @@ module.exports = {
|
||||||
shopNoProducts: "Encara no hi ha productes.",
|
shopNoProducts: "Encara no hi ha productes.",
|
||||||
shopOutOfStock: "Exhaurit",
|
shopOutOfStock: "Exhaurit",
|
||||||
shopBuy: "Comprar",
|
shopBuy: "Comprar",
|
||||||
shopBuyEncryptedNote: "Les teves dades de lliurament s'envien xifrades; només el propietari de la botiga les pot veure.",
|
|
||||||
shopBuyDeliveryAddress: "Adreça de lliurament",
|
shopBuyDeliveryAddress: "Adreça de lliurament",
|
||||||
shopBuyContact: "Contacte",
|
shopBuyContact: "Contacte",
|
||||||
shopBuyContactPlaceholder: "correu, telèfon, etc.",
|
shopBuyContactPlaceholder: "correu, telèfon, etc.",
|
||||||
|
|
@ -3022,6 +2993,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Descripció",
|
industryDescriptionLabel: "Descripció",
|
||||||
industryDescriptionPlaceholder: "Què produeix aquesta fàbrica?",
|
industryDescriptionPlaceholder: "Què produeix aquesta fàbrica?",
|
||||||
industrySector: "Sector",
|
industrySector: "Sector",
|
||||||
|
industrySector_software: "Programari",
|
||||||
|
industrySector_hardware: "Maquinari",
|
||||||
|
industrySector_agriculture: "Agricultura",
|
||||||
|
industrySector_textile: "Tèxtil",
|
||||||
|
industrySector_energy: "Energia",
|
||||||
|
industrySector_food: "Alimentació",
|
||||||
|
industrySector_construction: "Construcció",
|
||||||
|
industrySector_media: "Mitjans",
|
||||||
|
industrySector_services: "Serveis",
|
||||||
|
industrySector_other: "Altres",
|
||||||
industryMembershipPolicy: "Política de pertinença",
|
industryMembershipPolicy: "Política de pertinença",
|
||||||
industryQuorum: "Quòrum",
|
industryQuorum: "Quòrum",
|
||||||
industryMajority: "Majoria",
|
industryMajority: "Majoria",
|
||||||
|
|
@ -3223,6 +3204,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Mòdul per gestionar el teu contingut preferit.",
|
modulesFavoritesDescription: "Mòdul per gestionar el teu contingut preferit.",
|
||||||
fileTooLargeTitle: "Fitxer massa gran",
|
fileTooLargeTitle: "Fitxer massa gran",
|
||||||
fileTooLargeMessage: "El fitxer supera la mida màxima permesa (75 MB). Si us plau, selecciona un fitxer més petit.",
|
fileTooLargeMessage: "El fitxer supera la mida màxima permesa (75 MB). Si us plau, selecciona un fitxer més petit.",
|
||||||
|
fileWrongType: "Aquest fitxer no és del tipus adequat per a aquesta secció.",
|
||||||
|
mediaLockedAfterOpinions: "Ja té opinions: pots editar la resta, però no canviar el que s'ha valorat.",
|
||||||
|
pdfPrevPage: "Pàgina anterior",
|
||||||
|
pdfNextPage: "Pàgina següent",
|
||||||
|
pdfPage: "Pàgina",
|
||||||
|
pdfZoomIn: "Apropa",
|
||||||
|
pdfZoomOut: "Allunya",
|
||||||
|
pdfFullscreen: "Pantalla completa",
|
||||||
|
pdfClose: "Tanca",
|
||||||
|
pdfRotate: "Gira",
|
||||||
|
contentNotAvailableYet: "Aquest contingut encara no està disponible.",
|
||||||
|
pdfRetry: "Torna-ho a provar",
|
||||||
goBack: "Tornar",
|
goBack: "Tornar",
|
||||||
errorDismiss: "D'acord",
|
errorDismiss: "D'acord",
|
||||||
errorPageTitle: "S'ha produït un error",
|
errorPageTitle: "S'ha produït un error",
|
||||||
|
|
@ -3267,7 +3260,6 @@ module.exports = {
|
||||||
invitesFollow: "Seguir",
|
invitesFollow: "Seguir",
|
||||||
invitesUnfollowedInvites: "No federades",
|
invitesUnfollowedInvites: "No federades",
|
||||||
invitesNoUnfollowed: "No hi ha pubs no federats.",
|
invitesNoUnfollowed: "No hi ha pubs no federats.",
|
||||||
reportsWhyInappropriateLabel: "Per què és inadequat?",
|
|
||||||
blockchainContentDeleted: "Aquest contingut s'ha eliminat",
|
blockchainContentDeleted: "Aquest contingut s'ha eliminat",
|
||||||
visitContent: "Visitar el contingut",
|
visitContent: "Visitar el contingut",
|
||||||
favoriteAdd: "Fixar a preferits",
|
favoriteAdd: "Fixar a preferits",
|
||||||
|
|
@ -3579,6 +3571,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Introdueix etiquetes separades per comes",
|
podcastTagsPlaceholder: "Introdueix etiquetes separades per comes",
|
||||||
podcastFilterAll: "TOTS",
|
podcastFilterAll: "TOTS",
|
||||||
podcastFilterMine: "MEUS",
|
podcastFilterMine: "MEUS",
|
||||||
|
podcastFilterFavorites: "PREFERITS",
|
||||||
|
podcastEpisodeNumberLabel: "Número d'episodi",
|
||||||
podcastFilterRecent: "RECENTS",
|
podcastFilterRecent: "RECENTS",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "OIENTS",
|
podcastFilterViewers: "OIENTS",
|
||||||
|
|
@ -3797,7 +3791,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Tornar a la pàgina",
|
wikiBackToPage: "Tornar a la pàgina",
|
||||||
wikiLinkedChip: "Enllaçada",
|
wikiLinkedChip: "Enllaçada",
|
||||||
wikiRestoredSummary: "Versió restaurada",
|
wikiRestoredSummary: "Versió restaurada",
|
||||||
wikiExportPdf: "Generar la pàgina",
|
|
||||||
wikiPreview: "Previsualització",
|
wikiPreview: "Previsualització",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Crear una wiki",
|
tribeWikiCreate: "Crear una wiki",
|
||||||
|
|
@ -3865,6 +3858,9 @@ module.exports = {
|
||||||
roomRecStop: "Atura la gravació",
|
roomRecStop: "Atura la gravació",
|
||||||
roomStreamFile: "Emet",
|
roomStreamFile: "Emet",
|
||||||
roomStreamStop: "Atura",
|
roomStreamStop: "Atura",
|
||||||
|
roomScreen: "Pantalla",
|
||||||
|
roomScreenSharing: "Compartint pantalla",
|
||||||
|
roomScreenWarning: "Tothom a la sala veurà la teva pantalla sencera fins que aturis.",
|
||||||
roomStreaming: "Emetent",
|
roomStreaming: "Emetent",
|
||||||
liveFullscreen: "Pantalla completa",
|
liveFullscreen: "Pantalla completa",
|
||||||
roomStreamPickTitle: "Tria un vídeo",
|
roomStreamPickTitle: "Tria un vídeo",
|
||||||
|
|
@ -3922,6 +3918,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Ja en formes part. Pots obrir-ho des del seu mòdul.",
|
inviteAlreadyMember: "Ja en formes part. Pots obrir-ho des del seu mòdul.",
|
||||||
actionFailed: "No s'ha pogut fer. Torna-ho a provar.",
|
actionFailed: "No s'ha pogut fer. Torna-ho a provar.",
|
||||||
publishTooShort: "La teva publicació és massa curta.",
|
publishTooShort: "La teva publicació és massa curta.",
|
||||||
|
publishEmpty: "Escriu alguna cosa o adjunta un fitxer.",
|
||||||
walletSendFailed: "No s'ha pogut enviar el pagament: {reason}",
|
walletSendFailed: "No s'ha pogut enviar el pagament: {reason}",
|
||||||
pixeliaCoordsError: "Aquestes coordenades són fora de la quadrícula.",
|
pixeliaCoordsError: "Aquestes coordenades són fora de la quadrícula.",
|
||||||
courtsErrTitle: "Cal un títol.",
|
courtsErrTitle: "Cal un títol.",
|
||||||
|
|
@ -4434,6 +4431,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Etiquetes",
|
torrentTagsLabel: "Etiquetes",
|
||||||
torrentTagsPlaceholder: "Introdueix etiquetes separades per comes",
|
torrentTagsPlaceholder: "Introdueix etiquetes separades per comes",
|
||||||
torrentSizeLabel: "Mida",
|
torrentSizeLabel: "Mida",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Fitxers",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Generar torrent",
|
torrentFromContent: "Generar torrent",
|
||||||
torrentFromContentMissing: "El fitxer encara no està disponible en aquest node. Torna-ho a provar d'aquí a una estona.",
|
torrentFromContentMissing: "El fitxer encara no està disponible en aquest node. Torna-ho a provar d'aquí a una estona.",
|
||||||
torrentDownloads: "Baixades",
|
torrentDownloads: "Baixades",
|
||||||
|
|
@ -4519,9 +4519,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Acceptar",
|
inhabitantsPendingAccept: "Acceptar",
|
||||||
inhabitantsPendingReject: "Rebutjar",
|
inhabitantsPendingReject: "Rebutjar",
|
||||||
tribeSectionGovernance: "GOVERN",
|
tribeSectionGovernance: "GOVERN",
|
||||||
tribePadInviteRequired: "No tens accés al pad. Demana una invitació per accedir al contingut.",
|
|
||||||
tribeChatInviteRequired: "No tens accés al xat. Demana una invitació per accedir al contingut.",
|
|
||||||
tribeGovernanceDesc: "Govern intern d'aquesta tribu.",
|
|
||||||
tribeGovernanceNoGov: "Sense govern actiu",
|
tribeGovernanceNoGov: "Sense govern actiu",
|
||||||
tribeGovernanceNoGovDesc: "Aquesta tribu encara no té govern. Proposa candidatures per iniciar el procés.",
|
tribeGovernanceNoGovDesc: "Aquesta tribu encara no té govern. Proposa candidatures per iniciar el procés.",
|
||||||
tribeGovCardTitle: "Govern actual",
|
tribeGovCardTitle: "Govern actual",
|
||||||
|
|
@ -4578,7 +4575,6 @@ module.exports = {
|
||||||
logsFilterMonth: "DARRER MES",
|
logsFilterMonth: "DARRER MES",
|
||||||
logsFilterWeek: "DARRERA SETMANA",
|
logsFilterWeek: "DARRERA SETMANA",
|
||||||
logsFilterToday: "AVUI",
|
logsFilterToday: "AVUI",
|
||||||
logsComments: "Comentaris",
|
|
||||||
logsAuthorEntries: "Entrades",
|
logsAuthorEntries: "Entrades",
|
||||||
logsCreate: "Crear una entrada",
|
logsCreate: "Crear una entrada",
|
||||||
logsExport: "Exportar la bitàcola",
|
logsExport: "Exportar la bitàcola",
|
||||||
|
|
@ -4657,6 +4653,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periòdicament aquest node a altres instàncies d'Oasis de la mateixa xarxa local.",
|
settingsLanDesc: "Anuncia periòdicament aquest node a altres instàncies d'Oasis de la mateixa xarxa local.",
|
||||||
settingsLanEnable: "Activar la difusió LAN",
|
settingsLanEnable: "Activar la difusió LAN",
|
||||||
|
settingsConnectionsTitle: "Connexions",
|
||||||
|
settingsConnectionsDesc: "Quants peers manté connectats aquest node alhora.",
|
||||||
settingsLanTitle: "Difusió LAN",
|
settingsLanTitle: "Difusió LAN",
|
||||||
settingsReplicationTitle: "Replicació",
|
settingsReplicationTitle: "Replicació",
|
||||||
settingsReplicationDesc: "Configura el nombre de salts que el teu node segueix des del teu propi feed en replicar contingut.",
|
settingsReplicationDesc: "Configura el nombre de salts que el teu node segueix des del teu propi feed en replicar contingut.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Mein Netzwerk",
|
graphosBackToMine: "← Mein Netzwerk",
|
||||||
graphosShowing: "Zeige",
|
graphosShowing: "Zeige",
|
||||||
graphosYou: "Du",
|
graphosYou: "Du",
|
||||||
graphosTotalNodes: "Knoten insgesamt",
|
graphosTotalNodes: "Gesamt",
|
||||||
|
onlinePlural: "Online",
|
||||||
mentions: "Erwähnungen",
|
mentions: "Erwähnungen",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Jede Nachricht, die dich @erwähnt"),
|
strong("Jede Nachricht, die dich @erwähnt"),
|
||||||
|
|
@ -418,6 +419,7 @@ module.exports = {
|
||||||
menuMedia: "Bibliothek",
|
menuMedia: "Bibliothek",
|
||||||
menuTools: "Werkzeuge",
|
menuTools: "Werkzeuge",
|
||||||
comment: "Kommentar",
|
comment: "Kommentar",
|
||||||
|
typeComment: "Kommentar",
|
||||||
subtopic: "Unterthema",
|
subtopic: "Unterthema",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Nicht mehr unterstützen",
|
unfollow: "Nicht mehr unterstützen",
|
||||||
|
|
@ -447,9 +449,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.",
|
"ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -725,10 +724,6 @@ module.exports = {
|
||||||
votesQuestionLabel: "Frage",
|
votesQuestionLabel: "Frage",
|
||||||
voteStatusOpen: "OFFEN",
|
voteStatusOpen: "OFFEN",
|
||||||
voteStatusClosed: "GESCHLOSSEN",
|
voteStatusClosed: "GESCHLOSSEN",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" hat kommentiert in ",
|
|
||||||
a({ href: parentUrl }, " Thread"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Kommentar zu @${authorName}s Beitrag`],
|
commentTitle: ({ authorName }) => [`Kommentar zu @${authorName}s Beitrag`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" hat ein Unterthema erstellt aus ",
|
" hat ein Unterthema erstellt aus ",
|
||||||
|
|
@ -863,6 +858,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "Das PUB zahlt im nächsten Zyklus und veröffentlicht einen UBI-Transfer, den du bestätigen kannst.",
|
bankRulesEligibility5: "Das PUB zahlt im nächsten Zyklus und veröffentlicht einen UBI-Transfer, den du bestätigen kannst.",
|
||||||
bankRulesEligibility6: "Fehlt die Adresse, ist der Feed zu jung, wurde der Monat abgelehnt oder hat das PUB keine Mittel, wird der Anspruch übersprungen und auf dem PUB protokolliert.",
|
bankRulesEligibility6: "Fehlt die Adresse, ist der Feed zu jung, wurde der Monat abgelehnt oder hat das PUB keine Mittel, wird der Anspruch übersprungen und auf dem PUB protokolliert.",
|
||||||
copyLinkTitle: "Link kopieren",
|
copyLinkTitle: "Link kopieren",
|
||||||
|
cardMenuTitle: "Weitere Aktionen",
|
||||||
|
shareContent: "Teilen",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "UBI ablehnen",
|
bankRefuseUBI: "UBI ablehnen",
|
||||||
|
|
@ -896,7 +893,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Deine ECOin-Wallet ist noch nicht eingerichtet.",
|
walletNotConfigured: "Deine ECOin-Wallet ist noch nicht eingerichtet.",
|
||||||
walletSettingsDescription: "Integriere Oasis mit deiner ECOin-Wallet.",
|
walletSettingsDescription: "Integriere Oasis mit deiner ECOin-Wallet.",
|
||||||
walletSettingsMobileDescription: "Verbinde Oasis mit der ECOin-Wallet-App auf diesem Gerät.",
|
walletSettingsMobileDescription: "Verbinde Oasis mit der ECOin-Wallet-App auf diesem Gerät.",
|
||||||
walletDownloadEcoin: "ECOin herunterladen!",
|
walletDownloadEcoin: "ECOin herunterladen",
|
||||||
walletRpcUrl: "RPC-URL",
|
walletRpcUrl: "RPC-URL",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Ungültiger Betrag",
|
invalid_amount: "Ungültiger Betrag",
|
||||||
|
|
@ -948,6 +945,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Aktualisieren",
|
bookmarkUpdateButton: "Aktualisieren",
|
||||||
bookmarkAddFavoriteButton: "Favorit hinzufügen",
|
bookmarkAddFavoriteButton: "Favorit hinzufügen",
|
||||||
bookmarkRemoveFavoriteButton: "Favorit entfernen",
|
bookmarkRemoveFavoriteButton: "Favorit entfernen",
|
||||||
|
bookmarkTitleLabel: "Titel",
|
||||||
bookmarkUrlLabel: "Link",
|
bookmarkUrlLabel: "Link",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Beschreibung",
|
bookmarkDescriptionLabel: "Beschreibung",
|
||||||
|
|
@ -1092,9 +1090,9 @@ module.exports = {
|
||||||
allCVs: "Lebensläufe",
|
allCVs: "Lebensläufe",
|
||||||
discoverPeople: "Entdecke Bewohner in deinem Netzwerk.",
|
discoverPeople: "Entdecke Bewohner in deinem Netzwerk.",
|
||||||
searchInhabitantsPlaceholder: "In Bewohner suchen...",
|
searchInhabitantsPlaceholder: "In Bewohner suchen...",
|
||||||
filterLocation: "BEWOHNER NACH ORT FILTERN …",
|
filterLocation: "Nach Ort suchen…",
|
||||||
filterLanguage: "BEWOHNER NACH SPRACHE FILTERN …",
|
filterLanguage: "Nach Sprache suchen…",
|
||||||
filterSkills: "BEWOHNER NACH FÄHIGKEITEN FILTERN …",
|
filterSkills: "Nach Fähigkeiten suchen…",
|
||||||
cvSkillsCount: "Fähigkeiten",
|
cvSkillsCount: "Fähigkeiten",
|
||||||
applyFilters: "Filter anwenden",
|
applyFilters: "Filter anwenden",
|
||||||
locationLabel: "Ort",
|
locationLabel: "Ort",
|
||||||
|
|
@ -1110,6 +1108,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Aktivität",
|
topactivitySectionTitle: "Top Aktivität",
|
||||||
"TOP INACTIVITYButton": "AM INAKTIVSTEN",
|
"TOP INACTIVITYButton": "AM INAKTIVSTEN",
|
||||||
topinactivitySectionTitle: "Am wenigsten aktive Bewohner",
|
topinactivitySectionTitle: "Am wenigsten aktive Bewohner",
|
||||||
|
contactsSectionTitle: "Kontakte",
|
||||||
blockedSectionTitle: "Blockiert",
|
blockedSectionTitle: "Blockiert",
|
||||||
gallerySectionTitle: "GALERIE",
|
gallerySectionTitle: "GALERIE",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1563,9 +1562,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Unbekanntes Datum",
|
voteUnknownDate: "Unbekanntes Datum",
|
||||||
voteDetailSectionTitle: 'Abstimmungsdetails',
|
voteDetailSectionTitle: 'Abstimmungsdetails',
|
||||||
voteCommentsLabel: 'Kommentare',
|
voteCommentsLabel: 'Kommentare',
|
||||||
voteCommentsForumButton: 'Diskussion eröffnen',
|
voteNoCommentsYet: 'Noch keine Kommentare.',
|
||||||
voteCommentsSectionTitle: 'Offene Diskussion',
|
|
||||||
voteNoCommentsYet: 'Noch keine Kommentare. Sei der Erste, der antwortet.',
|
|
||||||
commentsGoToLast: "Zum letzten Kommentar",
|
commentsGoToLast: "Zum letzten Kommentar",
|
||||||
voteNewCommentPlaceholder: 'Schreibe deinen Kommentar hier…',
|
voteNewCommentPlaceholder: 'Schreibe deinen Kommentar hier…',
|
||||||
voteNewCommentButton: 'Kommentar veröffentlichen',
|
voteNewCommentButton: 'Kommentar veröffentlichen',
|
||||||
|
|
@ -1692,7 +1689,6 @@ module.exports = {
|
||||||
pollVoteButton: "Abstimmen",
|
pollVoteButton: "Abstimmen",
|
||||||
pollChangeVote: "Stimme Ändern",
|
pollChangeVote: "Stimme Ändern",
|
||||||
pollVoters: "Abstimmende",
|
pollVoters: "Abstimmende",
|
||||||
pollComments: "Kommentare",
|
|
||||||
pollStatusLabel: "Status",
|
pollStatusLabel: "Status",
|
||||||
pollStatusOpen: "OFFEN",
|
pollStatusOpen: "OFFEN",
|
||||||
pollStatusClosed: "GESCHLOSSEN",
|
pollStatusClosed: "GESCHLOSSEN",
|
||||||
|
|
@ -1714,9 +1710,22 @@ module.exports = {
|
||||||
blogAllowComments: "Kommentare erlauben",
|
blogAllowComments: "Kommentare erlauben",
|
||||||
blogCommentsClosed: "Der Autor hat die Kommentare für diesen Blog geschlossen.",
|
blogCommentsClosed: "Der Autor hat die Kommentare für diesen Blog geschlossen.",
|
||||||
blogNoItems: "Keine Blogs gefunden.",
|
blogNoItems: "Keine Blogs gefunden.",
|
||||||
viewBlog: "Blog Ansehen",
|
|
||||||
forumTitle: "Foren",
|
forumTitle: "Foren",
|
||||||
forumCategoryLabel: "Kategorie",
|
forumCategoryLabel: "Kategorie",
|
||||||
|
forumCatGENERAL: "ALLGEMEIN",
|
||||||
|
forumCatPOLITICS: "POLITIK",
|
||||||
|
forumCatTECH: "TECHNIK",
|
||||||
|
forumCatSCIENCE: "WISSENSCHAFT",
|
||||||
|
forumCatMUSIC: "MUSIK",
|
||||||
|
forumCatART: "KUNST",
|
||||||
|
forumCatGAMING: "SPIELE",
|
||||||
|
forumCatBOOKS: "BÜCHER",
|
||||||
|
forumCatFILMS: "FILME",
|
||||||
|
forumCatPHILOSOPHY: "PHILOSOPHIE",
|
||||||
|
forumCatSOCIETY: "GESELLSCHAFT",
|
||||||
|
forumCatPRIVACY: "PRIVATSPHÄRE",
|
||||||
|
forumCatCYBERWARFARE: "CYBERKRIEG",
|
||||||
|
forumCatSURVIVALISM: "SURVIVALISMUS",
|
||||||
forumTitleLabel: "Titel",
|
forumTitleLabel: "Titel",
|
||||||
forumTitlePlaceholder: "Forentitel...",
|
forumTitlePlaceholder: "Forentitel...",
|
||||||
forumCreateButton: "Forum erstellen",
|
forumCreateButton: "Forum erstellen",
|
||||||
|
|
@ -1988,43 +1997,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Berichte in Prüfung",
|
reportsUnderReviewSectionTitle: "Berichte in Prüfung",
|
||||||
reportsResolvedSectionTitle: "Gelöste Berichte",
|
reportsResolvedSectionTitle: "Gelöste Berichte",
|
||||||
reportsInvalidSectionTitle: "Ungültige Berichte",
|
reportsInvalidSectionTitle: "Ungültige Berichte",
|
||||||
reportsBugTemplateTitle: 'Reproduktionsdetails (Fehler)',
|
|
||||||
reportsFeatureTemplateTitle: 'Details (Funktionen)',
|
|
||||||
reportsAbuseTemplateTitle: 'Details (Missbrauch)',
|
|
||||||
reportsContentTemplateTitle: 'Details (Inhaltsprobleme)',
|
|
||||||
reportsStepsToReproduceLabel: 'Reproduktionsschritte',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Erwartetes Ergebnis',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'Was sollte passieren?',
|
|
||||||
reportsActualBehaviorLabel: 'Tatsächliches Ergebnis',
|
|
||||||
reportsActualBehaviorPlaceholder: 'Was passiert tatsächlich?',
|
|
||||||
reportsEnvironmentLabel: 'Umgebung',
|
|
||||||
reportsEnvironmentPlaceholder: 'Version, Gerät, Betriebssystem, Browser, Einstellungen, Protokolle, etc.',
|
|
||||||
reportsReproduceRateLabel: 'Reproduktionsrate',
|
|
||||||
reportsReproduceRateAlways: 'Immer',
|
|
||||||
reportsReproduceRateOften: 'Häufig',
|
|
||||||
reportsReproduceRateSometimes: 'Manchmal',
|
|
||||||
reportsReproduceRateRarely: 'Selten',
|
|
||||||
reportsReproduceRateUnable: 'Nicht reproduzierbar',
|
|
||||||
reportsReproduceRateUnknown: 'Nicht angegeben',
|
|
||||||
reportsProblemStatementLabel: 'Problem / Bedarf',
|
|
||||||
reportsProblemStatementPlaceholder: 'Welches Problem löst diese Funktion?',
|
|
||||||
reportsUserStoryLabel: 'Bewohner-Geschichte',
|
|
||||||
reportsUserStoryPlaceholder: 'Als <Bewohner> möchte ich <Aktion>, damit <Nutzen>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Akzeptanzkriterien',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Gegeben...\n- Wenn...\n- Dann...',
|
|
||||||
reportsWhatHappenedLabel: 'Was ist passiert?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Beschreibe den Vorfall mit Kontext und ungefährem Datum.',
|
|
||||||
reportsReportedUserLabel: 'Gemeldeter Bewohner / Entität',
|
|
||||||
reportsReportedUserPlaceholder: '@Bewohner, Feed, ID, Link, etc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Beweise / Links',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Links, Nachrichten-IDs, Screenshots (falls zutreffend), etc.',
|
|
||||||
reportsContentLocationLabel: 'Wo sich der Inhalt befindet',
|
|
||||||
reportsContentLocationPlaceholder: 'Link, ID, Kanal, Thread, Autor, etc.',
|
|
||||||
reportsWhyInappropriateLabel: "Warum es unangemessen ist",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Grund und Auswirkung erklären.',
|
|
||||||
reportsRequestedActionLabel: 'Gewünschte Maßnahme',
|
|
||||||
reportsRequestedActionPlaceholder: 'Entfernen, verbergen, markieren, warnen, etc.',
|
|
||||||
tribesTitle: "Stämme",
|
tribesTitle: "Stämme",
|
||||||
tribeFilterAll: "ALLE",
|
tribeFilterAll: "ALLE",
|
||||||
tribeFilterRecent: "NEUESTE",
|
tribeFilterRecent: "NEUESTE",
|
||||||
|
|
@ -2268,6 +2240,7 @@ module.exports = {
|
||||||
voteWellResearched: "Gut recherchiert",
|
voteWellResearched: "Gut recherchiert",
|
||||||
voteNeedsSources: "Quellen nötig",
|
voteNeedsSources: "Quellen nötig",
|
||||||
voteWrong: "Falsch",
|
voteWrong: "Falsch",
|
||||||
|
voteOutdated: "Veraltet",
|
||||||
voteLowQuality: "Niedrige Qualität",
|
voteLowQuality: "Niedrige Qualität",
|
||||||
voteLove: "Liebe",
|
voteLove: "Liebe",
|
||||||
voteClear: "Leeren",
|
voteClear: "Leeren",
|
||||||
|
|
@ -2512,7 +2485,6 @@ module.exports = {
|
||||||
shopNoProducts: "Noch keine Produkte.",
|
shopNoProducts: "Noch keine Produkte.",
|
||||||
shopOutOfStock: "Ausverkauft",
|
shopOutOfStock: "Ausverkauft",
|
||||||
shopBuy: "Kaufen",
|
shopBuy: "Kaufen",
|
||||||
shopBuyEncryptedNote: "Deine Lieferdaten werden verschlüsselt übertragen; nur der Shop-Inhaber kann sie lesen.",
|
|
||||||
shopBuyDeliveryAddress: "Lieferadresse",
|
shopBuyDeliveryAddress: "Lieferadresse",
|
||||||
shopBuyContact: "Kontakt",
|
shopBuyContact: "Kontakt",
|
||||||
shopBuyContactPlaceholder: "E-Mail, Telefon, usw.",
|
shopBuyContactPlaceholder: "E-Mail, Telefon, usw.",
|
||||||
|
|
@ -3029,6 +3001,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Beschreibung",
|
industryDescriptionLabel: "Beschreibung",
|
||||||
industryDescriptionPlaceholder: "Was produziert diese Fabrik?",
|
industryDescriptionPlaceholder: "Was produziert diese Fabrik?",
|
||||||
industrySector: "Sektor",
|
industrySector: "Sektor",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Landwirtschaft",
|
||||||
|
industrySector_textile: "Textil",
|
||||||
|
industrySector_energy: "Energie",
|
||||||
|
industrySector_food: "Lebensmittel",
|
||||||
|
industrySector_construction: "Bauwesen",
|
||||||
|
industrySector_media: "Medien",
|
||||||
|
industrySector_services: "Dienstleistungen",
|
||||||
|
industrySector_other: "Sonstiges",
|
||||||
industryMembershipPolicy: "Mitgliedschaftsrichtlinie",
|
industryMembershipPolicy: "Mitgliedschaftsrichtlinie",
|
||||||
industryQuorum: "Quorum",
|
industryQuorum: "Quorum",
|
||||||
industryMajority: "Mehrheit",
|
industryMajority: "Mehrheit",
|
||||||
|
|
@ -3230,6 +3212,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Modul zum Verwalten deiner favorisierten Inhalte.",
|
modulesFavoritesDescription: "Modul zum Verwalten deiner favorisierten Inhalte.",
|
||||||
fileTooLargeTitle: "Datei zu groß",
|
fileTooLargeTitle: "Datei zu groß",
|
||||||
fileTooLargeMessage: "Die Datei überschreitet die maximal erlaubte Größe (75 MB). Bitte wähle eine kleinere Datei.",
|
fileTooLargeMessage: "Die Datei überschreitet die maximal erlaubte Größe (75 MB). Bitte wähle eine kleinere Datei.",
|
||||||
|
fileWrongType: "Diese Datei hat nicht den passenden Typ für diesen Bereich.",
|
||||||
|
mediaLockedAfterOpinions: "Es hat bereits Meinungen: Du kannst den Rest bearbeiten, aber nicht das, was bewertet wurde.",
|
||||||
|
pdfPrevPage: "Vorherige Seite",
|
||||||
|
pdfNextPage: "Nächste Seite",
|
||||||
|
pdfPage: "Seite",
|
||||||
|
pdfZoomIn: "Vergrößern",
|
||||||
|
pdfZoomOut: "Verkleinern",
|
||||||
|
pdfFullscreen: "Vollbild",
|
||||||
|
pdfClose: "Schließen",
|
||||||
|
pdfRotate: "Drehen",
|
||||||
|
contentNotAvailableYet: "Dieser Inhalt ist noch nicht verfügbar.",
|
||||||
|
pdfRetry: "Erneut versuchen",
|
||||||
goBack: "Zurück",
|
goBack: "Zurück",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Etwas ist schiefgelaufen",
|
errorPageTitle: "Etwas ist schiefgelaufen",
|
||||||
|
|
@ -3274,8 +3268,6 @@ module.exports = {
|
||||||
courtsRespondentInvalid: "Ungültiger Beklagter",
|
courtsRespondentInvalid: "Ungültiger Beklagter",
|
||||||
feedDetailTitle: "Feed-Details",
|
feedDetailTitle: "Feed-Details",
|
||||||
feedOpenDiscussion: "Diskussion eröffnen",
|
feedOpenDiscussion: "Diskussion eröffnen",
|
||||||
feedPostComment: "Kommentar veröffentlichen",
|
|
||||||
noComments: "Noch keine Kommentare.",
|
|
||||||
mapsLabel: "Karten",
|
mapsLabel: "Karten",
|
||||||
mapTitle: "Karten",
|
mapTitle: "Karten",
|
||||||
mapDescription: "Verwalten und teilen Sie Offline-Karten in Ihrem Netzwerk.",
|
mapDescription: "Verwalten und teilen Sie Offline-Karten in Ihrem Netzwerk.",
|
||||||
|
|
@ -3579,6 +3571,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Tags durch Kommas getrennt eingeben",
|
podcastTagsPlaceholder: "Tags durch Kommas getrennt eingeben",
|
||||||
podcastFilterAll: "ALLE",
|
podcastFilterAll: "ALLE",
|
||||||
podcastFilterMine: "MEINE",
|
podcastFilterMine: "MEINE",
|
||||||
|
podcastFilterFavorites: "FAVORITEN",
|
||||||
|
podcastEpisodeNumberLabel: "Folgennummer",
|
||||||
podcastFilterRecent: "NEU",
|
podcastFilterRecent: "NEU",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "HÖRER",
|
podcastFilterViewers: "HÖRER",
|
||||||
|
|
@ -3797,7 +3791,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Zurück zur Seite",
|
wikiBackToPage: "Zurück zur Seite",
|
||||||
wikiLinkedChip: "Verknüpft",
|
wikiLinkedChip: "Verknüpft",
|
||||||
wikiRestoredSummary: "Wiederhergestellte Version",
|
wikiRestoredSummary: "Wiederhergestellte Version",
|
||||||
wikiExportPdf: "Seite erzeugen",
|
|
||||||
wikiPreview: "Vorschau",
|
wikiPreview: "Vorschau",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Wiki erstellen",
|
tribeWikiCreate: "Wiki erstellen",
|
||||||
|
|
@ -3865,6 +3858,9 @@ module.exports = {
|
||||||
roomRecStop: "Aufnahme beenden",
|
roomRecStop: "Aufnahme beenden",
|
||||||
roomStreamFile: "Streamen",
|
roomStreamFile: "Streamen",
|
||||||
roomStreamStop: "Stoppen",
|
roomStreamStop: "Stoppen",
|
||||||
|
roomScreen: "Bildschirm",
|
||||||
|
roomScreenSharing: "Bildschirm wird geteilt",
|
||||||
|
roomScreenWarning: "Alle im Raum sehen deinen ganzen Bildschirm, bis du stoppst.",
|
||||||
roomStreaming: "Wird gestreamt",
|
roomStreaming: "Wird gestreamt",
|
||||||
liveFullscreen: "Vollbild",
|
liveFullscreen: "Vollbild",
|
||||||
roomStreamPickTitle: "Video auswählen",
|
roomStreamPickTitle: "Video auswählen",
|
||||||
|
|
@ -3922,6 +3918,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Du bist schon dabei. Du kannst es über sein Modul öffnen.",
|
inviteAlreadyMember: "Du bist schon dabei. Du kannst es über sein Modul öffnen.",
|
||||||
actionFailed: "Das hat nicht geklappt. Bitte versuche es erneut.",
|
actionFailed: "Das hat nicht geklappt. Bitte versuche es erneut.",
|
||||||
publishTooShort: "Dein Beitrag ist zu kurz.",
|
publishTooShort: "Dein Beitrag ist zu kurz.",
|
||||||
|
publishEmpty: "Schreibe etwas oder hänge eine Datei an.",
|
||||||
walletSendFailed: "Die Zahlung konnte nicht gesendet werden: {reason}",
|
walletSendFailed: "Die Zahlung konnte nicht gesendet werden: {reason}",
|
||||||
pixeliaCoordsError: "Diese Koordinaten liegen außerhalb des Rasters.",
|
pixeliaCoordsError: "Diese Koordinaten liegen außerhalb des Rasters.",
|
||||||
courtsErrTitle: "Ein Titel ist erforderlich.",
|
courtsErrTitle: "Ein Titel ist erforderlich.",
|
||||||
|
|
@ -4430,6 +4427,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Tags",
|
torrentTagsLabel: "Tags",
|
||||||
torrentTagsPlaceholder: "Tags durch Kommas getrennt eingeben",
|
torrentTagsPlaceholder: "Tags durch Kommas getrennt eingeben",
|
||||||
torrentSizeLabel: "Größe",
|
torrentSizeLabel: "Größe",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Dateien",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Torrent erzeugen",
|
torrentFromContent: "Torrent erzeugen",
|
||||||
torrentFromContentMissing: "Die Datei ist auf diesem Knoten noch nicht verfügbar. Versuche es später erneut.",
|
torrentFromContentMissing: "Die Datei ist auf diesem Knoten noch nicht verfügbar. Versuche es später erneut.",
|
||||||
torrentDownloads: "Downloads",
|
torrentDownloads: "Downloads",
|
||||||
|
|
@ -4515,9 +4515,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Akzeptieren",
|
inhabitantsPendingAccept: "Akzeptieren",
|
||||||
inhabitantsPendingReject: "Ablehnen",
|
inhabitantsPendingReject: "Ablehnen",
|
||||||
tribeSectionGovernance: "GOVERNANCE",
|
tribeSectionGovernance: "GOVERNANCE",
|
||||||
tribePadInviteRequired: "Kein Zugriff auf das Pad. Bitte um Einladung.",
|
|
||||||
tribeChatInviteRequired: "Kein Zugriff auf den Chat. Bitte um Einladung.",
|
|
||||||
tribeGovernanceDesc: "Interne Governance dieses Stammes.",
|
|
||||||
tribeGovernanceNoGov: "Keine aktive Regierung",
|
tribeGovernanceNoGov: "Keine aktive Regierung",
|
||||||
tribeGovernanceNoGovDesc: "Dieser Stamm hat noch keine Regierung gewählt.",
|
tribeGovernanceNoGovDesc: "Dieser Stamm hat noch keine Regierung gewählt.",
|
||||||
tribeGovCardTitle: "Aktuelle Regierung",
|
tribeGovCardTitle: "Aktuelle Regierung",
|
||||||
|
|
@ -4574,7 +4571,6 @@ module.exports = {
|
||||||
logsFilterMonth: "LETZTER MONAT",
|
logsFilterMonth: "LETZTER MONAT",
|
||||||
logsFilterWeek: "LETZTE WOCHE",
|
logsFilterWeek: "LETZTE WOCHE",
|
||||||
logsFilterToday: "HEUTE",
|
logsFilterToday: "HEUTE",
|
||||||
logsComments: "Kommentare",
|
|
||||||
logsAuthorEntries: "Einträge",
|
logsAuthorEntries: "Einträge",
|
||||||
logsCreate: "Eintrag erstellen",
|
logsCreate: "Eintrag erstellen",
|
||||||
logsExport: "Logbuch exportieren",
|
logsExport: "Logbuch exportieren",
|
||||||
|
|
@ -4652,6 +4648,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "WILL-Test",
|
larpWillTestTitle: "WILL-Test",
|
||||||
settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen.",
|
settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen.",
|
||||||
settingsLanEnable: "LAN-Broadcasting aktivieren",
|
settingsLanEnable: "LAN-Broadcasting aktivieren",
|
||||||
|
settingsConnectionsTitle: "Verbindungen",
|
||||||
|
settingsConnectionsDesc: "Wie viele Peers dieser Knoten gleichzeitig verbunden hält.",
|
||||||
settingsLanTitle: "LAN-Broadcasting",
|
settingsLanTitle: "LAN-Broadcasting",
|
||||||
settingsReplicationTitle: "Replikation",
|
settingsReplicationTitle: "Replikation",
|
||||||
settingsReplicationDesc: "Konfiguriere die Anzahl der Hops, die dein Peer von deinem eigenen Feed aus beim Replizieren von Inhalten folgt.",
|
settingsReplicationDesc: "Konfiguriere die Anzahl der Hops, die dein Peer von deinem eigenen Feed aus beim Replizieren von Inhalten folgt.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← My network",
|
graphosBackToMine: "← My network",
|
||||||
graphosShowing: "Showing",
|
graphosShowing: "Showing",
|
||||||
graphosYou: "You",
|
graphosYou: "You",
|
||||||
graphosTotalNodes: "Total nodes",
|
graphosTotalNodes: "Total",
|
||||||
|
onlinePlural: "Online",
|
||||||
mentions: "Mentions",
|
mentions: "Mentions",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Every message that @mention you"),
|
strong("Every message that @mention you"),
|
||||||
|
|
@ -430,6 +431,7 @@ module.exports = {
|
||||||
menuMedia: "Library",
|
menuMedia: "Library",
|
||||||
menuTools: "Tools",
|
menuTools: "Tools",
|
||||||
comment: "Comment",
|
comment: "Comment",
|
||||||
|
typeComment: "Comment",
|
||||||
subtopic: "Subtopic",
|
subtopic: "Subtopic",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Unsupport",
|
unfollow: "Unsupport",
|
||||||
|
|
@ -459,9 +461,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
|
"REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -513,6 +512,8 @@ module.exports = {
|
||||||
settingsBlobCacheCleanNow: "Clean now",
|
settingsBlobCacheCleanNow: "Clean now",
|
||||||
settingsBlobCacheCleaned: "{n} files removed, {size} freed.",
|
settingsBlobCacheCleaned: "{n} files removed, {size} freed.",
|
||||||
settingsLanEnable: "Enable LAN broadcasting",
|
settingsLanEnable: "Enable LAN broadcasting",
|
||||||
|
settingsConnectionsTitle: "Connections",
|
||||||
|
settingsConnectionsDesc: "How many peers this node keeps connected at the same time.",
|
||||||
saveSettings: "Save settings",
|
saveSettings: "Save settings",
|
||||||
exportDescription: "Set password (min 32 characters long) to encrypt your key",
|
exportDescription: "Set password (min 32 characters long) to encrypt your key",
|
||||||
exportDataTitle: "Instant Backup",
|
exportDataTitle: "Instant Backup",
|
||||||
|
|
@ -754,10 +755,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Question",
|
votesQuestionLabel:"Question",
|
||||||
voteStatusOpen:"OPEN",
|
voteStatusOpen:"OPEN",
|
||||||
voteStatusClosed:"CLOSED",
|
voteStatusClosed:"CLOSED",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" commented on ",
|
|
||||||
a({ href: parentUrl }, " thread"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Comment on @${authorName}'s post`],
|
commentTitle: ({ authorName }) => [`Comment on @${authorName}'s post`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" created a subtopic from ",
|
" created a subtopic from ",
|
||||||
|
|
@ -895,6 +892,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "The PUB pays on its next cycle and publishes a UBI transfer that you can confirm.",
|
bankRulesEligibility5: "The PUB pays on its next cycle and publishes a UBI transfer that you can confirm.",
|
||||||
bankRulesEligibility6: "If your address is missing, the feed is too young, you refused the month or the PUB has no funds, the claim is skipped and logged on the PUB.",
|
bankRulesEligibility6: "If your address is missing, the feed is too young, you refused the month or the PUB has no funds, the claim is skipped and logged on the PUB.",
|
||||||
copyLinkTitle: "Copy link",
|
copyLinkTitle: "Copy link",
|
||||||
|
cardMenuTitle: "More actions",
|
||||||
|
shareContent: "Share",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Refuse UBI",
|
bankRefuseUBI: "Refuse UBI",
|
||||||
|
|
@ -928,7 +927,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Your ECOin wallet is not configured yet.",
|
walletNotConfigured: "Your ECOin wallet is not configured yet.",
|
||||||
walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
|
walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
|
||||||
walletSettingsMobileDescription: "Link Oasis with the ECOin Wallet app on this device.",
|
walletSettingsMobileDescription: "Link Oasis with the ECOin Wallet app on this device.",
|
||||||
walletDownloadEcoin: "Download ECOin!",
|
walletDownloadEcoin: "Download ECOin",
|
||||||
walletRpcUrl: "RPC URL",
|
walletRpcUrl: "RPC URL",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Invalid amount",
|
invalid_amount: "Invalid amount",
|
||||||
|
|
@ -981,6 +980,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Update",
|
bookmarkUpdateButton: "Update",
|
||||||
bookmarkAddFavoriteButton: "Add favorite",
|
bookmarkAddFavoriteButton: "Add favorite",
|
||||||
bookmarkRemoveFavoriteButton: "Remove favorite",
|
bookmarkRemoveFavoriteButton: "Remove favorite",
|
||||||
|
bookmarkTitleLabel: "Title",
|
||||||
bookmarkUrlLabel: "Link",
|
bookmarkUrlLabel: "Link",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Description",
|
bookmarkDescriptionLabel: "Description",
|
||||||
|
|
@ -1212,9 +1212,9 @@ module.exports = {
|
||||||
allCVs: "CVs",
|
allCVs: "CVs",
|
||||||
discoverPeople: "Discover inhabitants in your network.",
|
discoverPeople: "Discover inhabitants in your network.",
|
||||||
searchInhabitantsPlaceholder: "Search in Inhabitants...",
|
searchInhabitantsPlaceholder: "Search in Inhabitants...",
|
||||||
filterLocation: "FILTER inhabitants BY LOCATION …",
|
filterLocation: "Search by location…",
|
||||||
filterLanguage: "FILTER inhabitants BY LANGUAGE …",
|
filterLanguage: "Search by language…",
|
||||||
filterSkills: "FILTER inhabitants BY SKILLS …",
|
filterSkills: "Search by skills…",
|
||||||
cvSkillsCount: "Skills",
|
cvSkillsCount: "Skills",
|
||||||
applyFilters: "Apply Filters",
|
applyFilters: "Apply Filters",
|
||||||
locationLabel: "Location",
|
locationLabel: "Location",
|
||||||
|
|
@ -1230,6 +1230,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Activity",
|
topactivitySectionTitle: "Top Activity",
|
||||||
"TOP INACTIVITYButton": "TOP INACTIVITY",
|
"TOP INACTIVITYButton": "TOP INACTIVITY",
|
||||||
topinactivitySectionTitle: "Least active inhabitants",
|
topinactivitySectionTitle: "Least active inhabitants",
|
||||||
|
contactsSectionTitle: "Contacts",
|
||||||
blockedSectionTitle: "Blocked",
|
blockedSectionTitle: "Blocked",
|
||||||
gallerySectionTitle: "GALLERY",
|
gallerySectionTitle: "GALLERY",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1684,9 +1685,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Unknown Date",
|
voteUnknownDate: "Unknown Date",
|
||||||
voteDetailSectionTitle: 'Vote details',
|
voteDetailSectionTitle: 'Vote details',
|
||||||
voteCommentsLabel: 'Comments',
|
voteCommentsLabel: 'Comments',
|
||||||
voteCommentsForumButton: 'Open discussion',
|
voteNoCommentsYet: 'There are no comments yet.',
|
||||||
voteCommentsSectionTitle: 'Open discussion',
|
|
||||||
voteNoCommentsYet: 'There are no comments yet. Be the first to reply.',
|
|
||||||
commentsGoToLast: "Go to the last comment",
|
commentsGoToLast: "Go to the last comment",
|
||||||
voteNewCommentPlaceholder: 'Write your comment here…',
|
voteNewCommentPlaceholder: 'Write your comment here…',
|
||||||
voteNewCommentButton: 'Post comment',
|
voteNewCommentButton: 'Post comment',
|
||||||
|
|
@ -1813,7 +1812,6 @@ module.exports = {
|
||||||
pollVoteButton: "Vote",
|
pollVoteButton: "Vote",
|
||||||
pollChangeVote: "Change Vote",
|
pollChangeVote: "Change Vote",
|
||||||
pollVoters: "Voters",
|
pollVoters: "Voters",
|
||||||
pollComments: "Comments",
|
|
||||||
pollStatusLabel: "Status",
|
pollStatusLabel: "Status",
|
||||||
pollStatusOpen: "OPEN",
|
pollStatusOpen: "OPEN",
|
||||||
pollStatusClosed: "CLOSED",
|
pollStatusClosed: "CLOSED",
|
||||||
|
|
@ -1835,9 +1833,22 @@ module.exports = {
|
||||||
blogAllowComments: "Allow comments",
|
blogAllowComments: "Allow comments",
|
||||||
blogCommentsClosed: "The author has closed comments on this blog.",
|
blogCommentsClosed: "The author has closed comments on this blog.",
|
||||||
blogNoItems: "No blogs found.",
|
blogNoItems: "No blogs found.",
|
||||||
viewBlog: "View Blog",
|
|
||||||
forumTitle: "Forums",
|
forumTitle: "Forums",
|
||||||
forumCategoryLabel: "Category",
|
forumCategoryLabel: "Category",
|
||||||
|
forumCatGENERAL: "GENERAL",
|
||||||
|
forumCatPOLITICS: "POLITICS",
|
||||||
|
forumCatTECH: "TECH",
|
||||||
|
forumCatSCIENCE: "SCIENCE",
|
||||||
|
forumCatMUSIC: "MUSIC",
|
||||||
|
forumCatART: "ART",
|
||||||
|
forumCatGAMING: "GAMING",
|
||||||
|
forumCatBOOKS: "BOOKS",
|
||||||
|
forumCatFILMS: "FILMS",
|
||||||
|
forumCatPHILOSOPHY: "PHILOSOPHY",
|
||||||
|
forumCatSOCIETY: "SOCIETY",
|
||||||
|
forumCatPRIVACY: "PRIVACY",
|
||||||
|
forumCatCYBERWARFARE: "CYBERWARFARE",
|
||||||
|
forumCatSURVIVALISM: "SURVIVALISM",
|
||||||
forumTitleLabel: "Title",
|
forumTitleLabel: "Title",
|
||||||
forumTitlePlaceholder: "Forum title...",
|
forumTitlePlaceholder: "Forum title...",
|
||||||
forumCreateButton: "Create forum",
|
forumCreateButton: "Create forum",
|
||||||
|
|
@ -2026,8 +2037,6 @@ module.exports = {
|
||||||
feedTitle: "Feed",
|
feedTitle: "Feed",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Open discussion",
|
feedOpenDiscussion: "Open discussion",
|
||||||
feedPostComment: "Post comment",
|
|
||||||
noComments: "No comments yet",
|
|
||||||
createFeedTitle: "Create Feed",
|
createFeedTitle: "Create Feed",
|
||||||
createFeedButton: "Send Feed!",
|
createFeedButton: "Send Feed!",
|
||||||
feedPlaceholder: "What's happening? (max 280 characters)",
|
feedPlaceholder: "What's happening? (max 280 characters)",
|
||||||
|
|
@ -2248,43 +2257,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Under Review Reports",
|
reportsUnderReviewSectionTitle: "Under Review Reports",
|
||||||
reportsResolvedSectionTitle: "Resolved Reports",
|
reportsResolvedSectionTitle: "Resolved Reports",
|
||||||
reportsInvalidSectionTitle: "Invalid Reports",
|
reportsInvalidSectionTitle: "Invalid Reports",
|
||||||
reportsBugTemplateTitle: 'Reproduction details (Bugs)',
|
|
||||||
reportsFeatureTemplateTitle: 'Details (Features)',
|
|
||||||
reportsAbuseTemplateTitle: 'Details (Abuse)',
|
|
||||||
reportsContentTemplateTitle: 'Details (Content Issues)',
|
|
||||||
reportsStepsToReproduceLabel: 'Steps to reproduce',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Expected result',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'What should happen?',
|
|
||||||
reportsActualBehaviorLabel: 'Actual result',
|
|
||||||
reportsActualBehaviorPlaceholder: 'What actually happens?',
|
|
||||||
reportsEnvironmentLabel: 'Environment',
|
|
||||||
reportsEnvironmentPlaceholder: 'Version, device, OS, browser, settings, logs, etc.',
|
|
||||||
reportsReproduceRateLabel: 'Reproduction rate',
|
|
||||||
reportsReproduceRateAlways: 'Always',
|
|
||||||
reportsReproduceRateOften: 'Often',
|
|
||||||
reportsReproduceRateSometimes: 'Sometimes',
|
|
||||||
reportsReproduceRateRarely: 'Rarely',
|
|
||||||
reportsReproduceRateUnable: 'Unable to reproduce',
|
|
||||||
reportsReproduceRateUnknown: 'Not specified',
|
|
||||||
reportsProblemStatementLabel: 'Problem / need',
|
|
||||||
reportsProblemStatementPlaceholder: 'What problem does this feature solve?',
|
|
||||||
reportsUserStoryLabel: 'Inhabitant story',
|
|
||||||
reportsUserStoryPlaceholder: 'As a <inhabitant>, I want <action>, so that <benefit>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Acceptance criteria',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Given...\n- When...\n- Then...',
|
|
||||||
reportsWhatHappenedLabel: 'What happened?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Describe the incident with context and approximate dates.',
|
|
||||||
reportsReportedUserLabel: 'Reported inhabitant / entity',
|
|
||||||
reportsReportedUserPlaceholder: '@inhabitant, feed, ID, link, etc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Evidence / links',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Paste links, message IDs, screenshots (if applicable), etc.',
|
|
||||||
reportsContentLocationLabel: 'Where the content is',
|
|
||||||
reportsContentLocationPlaceholder: 'Link, ID, channel, thread, author, etc.',
|
|
||||||
reportsWhyInappropriateLabel: "Why it's inappropriate",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Explain the reason and impact.',
|
|
||||||
reportsRequestedActionLabel: 'Requested action',
|
|
||||||
reportsRequestedActionPlaceholder: 'Remove, hide, tag, warn, etc.',
|
|
||||||
tribesTitle: "Tribes",
|
tribesTitle: "Tribes",
|
||||||
tribeFilterAll: "ALL",
|
tribeFilterAll: "ALL",
|
||||||
tribeFilterRecent: "RECENT",
|
tribeFilterRecent: "RECENT",
|
||||||
|
|
@ -2528,6 +2500,7 @@ module.exports = {
|
||||||
voteWellResearched: "Well researched",
|
voteWellResearched: "Well researched",
|
||||||
voteNeedsSources: "Needs sources",
|
voteNeedsSources: "Needs sources",
|
||||||
voteWrong: "Wrong",
|
voteWrong: "Wrong",
|
||||||
|
voteOutdated: "Outdated",
|
||||||
voteLowQuality: "Low quality",
|
voteLowQuality: "Low quality",
|
||||||
voteLove: "Love",
|
voteLove: "Love",
|
||||||
voteClear: "Clear",
|
voteClear: "Clear",
|
||||||
|
|
@ -2772,7 +2745,6 @@ module.exports = {
|
||||||
shopNoProducts: "No products yet.",
|
shopNoProducts: "No products yet.",
|
||||||
shopOutOfStock: "Out of stock",
|
shopOutOfStock: "Out of stock",
|
||||||
shopBuy: "Buy",
|
shopBuy: "Buy",
|
||||||
shopBuyEncryptedNote: "Your delivery details are sent encrypted, visible only to the shop owner.",
|
|
||||||
shopBuyDeliveryAddress: "Delivery address",
|
shopBuyDeliveryAddress: "Delivery address",
|
||||||
shopBuyContact: "Contact",
|
shopBuyContact: "Contact",
|
||||||
shopBuyContactPlaceholder: "email, phone, etc.",
|
shopBuyContactPlaceholder: "email, phone, etc.",
|
||||||
|
|
@ -3289,6 +3261,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Description",
|
industryDescriptionLabel: "Description",
|
||||||
industryDescriptionPlaceholder: "What does this facility produce?",
|
industryDescriptionPlaceholder: "What does this facility produce?",
|
||||||
industrySector: "Sector",
|
industrySector: "Sector",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Agriculture",
|
||||||
|
industrySector_textile: "Textile",
|
||||||
|
industrySector_energy: "Energy",
|
||||||
|
industrySector_food: "Food",
|
||||||
|
industrySector_construction: "Construction",
|
||||||
|
industrySector_media: "Media",
|
||||||
|
industrySector_services: "Services",
|
||||||
|
industrySector_other: "Other",
|
||||||
industryMembershipPolicy: "Membership policy",
|
industryMembershipPolicy: "Membership policy",
|
||||||
industryQuorum: "Quorum",
|
industryQuorum: "Quorum",
|
||||||
industryMajority: "Majority",
|
industryMajority: "Majority",
|
||||||
|
|
@ -3490,6 +3472,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Module to manage your favorite content.",
|
modulesFavoritesDescription: "Module to manage your favorite content.",
|
||||||
fileTooLargeTitle: "File too large",
|
fileTooLargeTitle: "File too large",
|
||||||
fileTooLargeMessage: "The file exceeds the maximum allowed size (75 MB). Please select a smaller file.",
|
fileTooLargeMessage: "The file exceeds the maximum allowed size (75 MB). Please select a smaller file.",
|
||||||
|
fileWrongType: "This file is not the right type for this section.",
|
||||||
|
mediaLockedAfterOpinions: "It already has opinions: you can edit the rest, but not change what was rated.",
|
||||||
|
pdfPrevPage: "Previous page",
|
||||||
|
pdfNextPage: "Next page",
|
||||||
|
pdfPage: "Page",
|
||||||
|
pdfZoomIn: "Zoom in",
|
||||||
|
pdfZoomOut: "Zoom out",
|
||||||
|
pdfFullscreen: "Full screen",
|
||||||
|
pdfClose: "Close",
|
||||||
|
pdfRotate: "Rotate",
|
||||||
|
contentNotAvailableYet: "This content is not available, yet.",
|
||||||
|
pdfRetry: "Retry",
|
||||||
goBack: "Go back",
|
goBack: "Go back",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Something went wrong",
|
errorPageTitle: "Something went wrong",
|
||||||
|
|
@ -3834,6 +3828,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Enter tags separated by commas",
|
podcastTagsPlaceholder: "Enter tags separated by commas",
|
||||||
podcastFilterAll: "ALL",
|
podcastFilterAll: "ALL",
|
||||||
podcastFilterMine: "MINE",
|
podcastFilterMine: "MINE",
|
||||||
|
podcastFilterFavorites: "FAVORITES",
|
||||||
|
podcastEpisodeNumberLabel: "Episode number",
|
||||||
podcastFilterRecent: "RECENT",
|
podcastFilterRecent: "RECENT",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "VIEWERS",
|
podcastFilterViewers: "VIEWERS",
|
||||||
|
|
@ -4052,7 +4048,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Back to page",
|
wikiBackToPage: "Back to page",
|
||||||
wikiLinkedChip: "Linked",
|
wikiLinkedChip: "Linked",
|
||||||
wikiRestoredSummary: "Restored version",
|
wikiRestoredSummary: "Restored version",
|
||||||
wikiExportPdf: "Generate Page",
|
|
||||||
wikiPreview: "Preview",
|
wikiPreview: "Preview",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Create Wiki",
|
tribeWikiCreate: "Create Wiki",
|
||||||
|
|
@ -4121,6 +4116,9 @@ module.exports = {
|
||||||
roomRecStop: "Stop recording",
|
roomRecStop: "Stop recording",
|
||||||
roomStreamFile: "Stream",
|
roomStreamFile: "Stream",
|
||||||
roomStreamStop: "Stop",
|
roomStreamStop: "Stop",
|
||||||
|
roomScreen: "Screen",
|
||||||
|
roomScreenSharing: "Sharing screen",
|
||||||
|
roomScreenWarning: "Everyone in the room will see your whole screen until you stop.",
|
||||||
roomStreaming: "Streaming",
|
roomStreaming: "Streaming",
|
||||||
liveFullscreen: "Full screen",
|
liveFullscreen: "Full screen",
|
||||||
roomStreamPickTitle: "Choose a video",
|
roomStreamPickTitle: "Choose a video",
|
||||||
|
|
@ -4178,6 +4176,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "You are already in. You can open it from its module.",
|
inviteAlreadyMember: "You are already in. You can open it from its module.",
|
||||||
actionFailed: "That could not be done. Please try again.",
|
actionFailed: "That could not be done. Please try again.",
|
||||||
publishTooShort: "Your post is too short.",
|
publishTooShort: "Your post is too short.",
|
||||||
|
publishEmpty: "Write something or attach a file.",
|
||||||
walletSendFailed: "The payment could not be sent: {reason}",
|
walletSendFailed: "The payment could not be sent: {reason}",
|
||||||
pixeliaCoordsError: "Those coordinates are outside the grid.",
|
pixeliaCoordsError: "Those coordinates are outside the grid.",
|
||||||
courtsErrTitle: "A title is required.",
|
courtsErrTitle: "A title is required.",
|
||||||
|
|
@ -4751,6 +4750,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Tags",
|
torrentTagsLabel: "Tags",
|
||||||
torrentTagsPlaceholder: "Enter tags separated by commas",
|
torrentTagsPlaceholder: "Enter tags separated by commas",
|
||||||
torrentSizeLabel: "Size",
|
torrentSizeLabel: "Size",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Files",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Generate torrent",
|
torrentFromContent: "Generate torrent",
|
||||||
torrentFromContentMissing: "The file is not available on this node yet. Try again in a while.",
|
torrentFromContentMissing: "The file is not available on this node yet. Try again in a while.",
|
||||||
torrentDownloads: "Downloads",
|
torrentDownloads: "Downloads",
|
||||||
|
|
@ -4836,9 +4838,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Accept",
|
inhabitantsPendingAccept: "Accept",
|
||||||
inhabitantsPendingReject: "Reject",
|
inhabitantsPendingReject: "Reject",
|
||||||
tribeSectionGovernance: "GOVERNANCE",
|
tribeSectionGovernance: "GOVERNANCE",
|
||||||
tribePadInviteRequired: "You do not have access to the pad. Ask for an invitation to access the content.",
|
|
||||||
tribeChatInviteRequired: "You do not have access to the chat. Ask for an invitation to access the content.",
|
|
||||||
tribeGovernanceDesc: "Internal governance for this tribe. Propose candidatures, debate rules, elect leaders.",
|
|
||||||
tribeGovernanceNoGov: "No active government",
|
tribeGovernanceNoGov: "No active government",
|
||||||
tribeGovernanceNoGovDesc: "This tribe has not yet elected a government. Propose candidatures to start the process.",
|
tribeGovernanceNoGovDesc: "This tribe has not yet elected a government. Propose candidatures to start the process.",
|
||||||
tribeGovCardTitle: "Current Government",
|
tribeGovCardTitle: "Current Government",
|
||||||
|
|
@ -4895,7 +4894,6 @@ module.exports = {
|
||||||
logsFilterMonth: "LAST MONTH",
|
logsFilterMonth: "LAST MONTH",
|
||||||
logsFilterWeek: "LAST WEEK",
|
logsFilterWeek: "LAST WEEK",
|
||||||
logsFilterToday: "TODAY",
|
logsFilterToday: "TODAY",
|
||||||
logsComments: "Comments",
|
|
||||||
logsAuthorEntries: "Entries",
|
logsAuthorEntries: "Entries",
|
||||||
logsCreate: "Create Log",
|
logsCreate: "Create Log",
|
||||||
logsExport: "Export Logs",
|
logsExport: "Export Logs",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Mi red",
|
graphosBackToMine: "← Mi red",
|
||||||
graphosShowing: "Mostrando",
|
graphosShowing: "Mostrando",
|
||||||
graphosYou: "Tú",
|
graphosYou: "Tú",
|
||||||
graphosTotalNodes: "Nodos totales",
|
graphosTotalNodes: "Totales",
|
||||||
|
onlinePlural: "Conectados",
|
||||||
mentions: "Menciones",
|
mentions: "Menciones",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Todos los mensajes que te @mencionan"),
|
strong("Todos los mensajes que te @mencionan"),
|
||||||
|
|
@ -412,6 +413,7 @@ module.exports = {
|
||||||
menuMedia: "Biblioteca",
|
menuMedia: "Biblioteca",
|
||||||
menuTools: "Herramientas",
|
menuTools: "Herramientas",
|
||||||
comment: "Comentar",
|
comment: "Comentar",
|
||||||
|
typeComment: "Comentario",
|
||||||
subtopic: "Subtema",
|
subtopic: "Subtema",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Dejar de soportar",
|
unfollow: "Dejar de soportar",
|
||||||
|
|
@ -441,9 +443,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!",
|
"RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -718,10 +717,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Pregunta",
|
votesQuestionLabel:"Pregunta",
|
||||||
voteStatusOpen:"ABIERTO",
|
voteStatusOpen:"ABIERTO",
|
||||||
voteStatusClosed:"CERRADO",
|
voteStatusClosed:"CERRADO",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" ha comentado en ",
|
|
||||||
a({ href: parentUrl }, " hilo"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Ha comentado en el post de @${authorName}`],
|
commentTitle: ({ authorName }) => [`Ha comentado en el post de @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" ha creado un subtema desde ",
|
" ha creado un subtema desde ",
|
||||||
|
|
@ -856,6 +851,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "El PUB paga en su siguiente ciclo y publica un transfer RBU que puedes confirmar.",
|
bankRulesEligibility5: "El PUB paga en su siguiente ciclo y publica un transfer RBU que puedes confirmar.",
|
||||||
bankRulesEligibility6: "Si falta tu dirección, el feed es demasiado joven, rechazaste el mes o el PUB no tiene fondos, el reclamo se salta y queda registrado en el PUB.",
|
bankRulesEligibility6: "Si falta tu dirección, el feed es demasiado joven, rechazaste el mes o el PUB no tiene fondos, el reclamo se salta y queda registrado en el PUB.",
|
||||||
copyLinkTitle: "Copiar enlace",
|
copyLinkTitle: "Copiar enlace",
|
||||||
|
cardMenuTitle: "Más acciones",
|
||||||
|
shareContent: "Compartir",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Rechazar RBU",
|
bankRefuseUBI: "Rechazar RBU",
|
||||||
|
|
@ -889,7 +886,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Tu cartera de ECOin aún no está configurada.",
|
walletNotConfigured: "Tu cartera de ECOin aún no está configurada.",
|
||||||
walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.",
|
walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.",
|
||||||
walletSettingsMobileDescription: "Vincula Oasis con la app ECOin Wallet de este dispositivo.",
|
walletSettingsMobileDescription: "Vincula Oasis con la app ECOin Wallet de este dispositivo.",
|
||||||
walletDownloadEcoin: "¡Descargar ECOin!",
|
walletDownloadEcoin: "Descargar ECOin",
|
||||||
walletRpcUrl: "URL del RPC",
|
walletRpcUrl: "URL del RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Cantidad inválida",
|
invalid_amount: "Cantidad inválida",
|
||||||
|
|
@ -942,6 +939,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Actualizar",
|
bookmarkUpdateButton: "Actualizar",
|
||||||
bookmarkAddFavoriteButton: "Añadir favorito",
|
bookmarkAddFavoriteButton: "Añadir favorito",
|
||||||
bookmarkRemoveFavoriteButton: "Quitar favorito",
|
bookmarkRemoveFavoriteButton: "Quitar favorito",
|
||||||
|
bookmarkTitleLabel: "Título",
|
||||||
bookmarkUrlLabel: "Enlace",
|
bookmarkUrlLabel: "Enlace",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Descripción",
|
bookmarkDescriptionLabel: "Descripción",
|
||||||
|
|
@ -1086,9 +1084,9 @@ module.exports = {
|
||||||
allCVs: "CVs",
|
allCVs: "CVs",
|
||||||
discoverPeople: "Descubre habitantes en tu red.",
|
discoverPeople: "Descubre habitantes en tu red.",
|
||||||
searchInhabitantsPlaceholder: "Buscar en Habitantes...",
|
searchInhabitantsPlaceholder: "Buscar en Habitantes...",
|
||||||
filterLocation: "FILTRAR habitantes POR UBICACIÓN …",
|
filterLocation: "Buscar por ubicación…",
|
||||||
filterLanguage: "FILTRAR habitantes POR IDIOMA …",
|
filterLanguage: "Buscar por idioma…",
|
||||||
filterSkills: "FILTRAR habitantes POR HABILIDADES …",
|
filterSkills: "Buscar por habilidades…",
|
||||||
cvSkillsCount: "Habilidades",
|
cvSkillsCount: "Habilidades",
|
||||||
applyFilters: "Aplicar Filtros",
|
applyFilters: "Aplicar Filtros",
|
||||||
locationLabel: "Ubicación",
|
locationLabel: "Ubicación",
|
||||||
|
|
@ -1104,6 +1102,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Actividad",
|
topactivitySectionTitle: "Top Actividad",
|
||||||
"TOP INACTIVITYButton": "MÁS INACTIVOS",
|
"TOP INACTIVITYButton": "MÁS INACTIVOS",
|
||||||
topinactivitySectionTitle: "Habitantes menos activos",
|
topinactivitySectionTitle: "Habitantes menos activos",
|
||||||
|
contactsSectionTitle: "Contactos",
|
||||||
blockedSectionTitle: "Bloqueadas",
|
blockedSectionTitle: "Bloqueadas",
|
||||||
gallerySectionTitle: "GALERÍA",
|
gallerySectionTitle: "GALERÍA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1558,9 +1557,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Fecha Desconocida",
|
voteUnknownDate: "Fecha Desconocida",
|
||||||
voteDetailSectionTitle: 'Detalles de la votación',
|
voteDetailSectionTitle: 'Detalles de la votación',
|
||||||
voteCommentsLabel: 'Comentarios',
|
voteCommentsLabel: 'Comentarios',
|
||||||
voteCommentsForumButton: 'Abrir conversación',
|
voteNoCommentsYet: 'Todavía no hay comentarios.',
|
||||||
voteCommentsSectionTitle: 'Conversación abierta',
|
|
||||||
voteNoCommentsYet: 'Todavía no hay comentarios. Sé el primer habitante en opinar.',
|
|
||||||
commentsGoToLast: "Ir al último comentario",
|
commentsGoToLast: "Ir al último comentario",
|
||||||
voteNewCommentPlaceholder: 'Escribe aquí tu comentario…',
|
voteNewCommentPlaceholder: 'Escribe aquí tu comentario…',
|
||||||
voteNewCommentButton: 'Publicar comentario',
|
voteNewCommentButton: 'Publicar comentario',
|
||||||
|
|
@ -1687,7 +1684,6 @@ module.exports = {
|
||||||
pollVoteButton: "Votar",
|
pollVoteButton: "Votar",
|
||||||
pollChangeVote: "Cambiar Voto",
|
pollChangeVote: "Cambiar Voto",
|
||||||
pollVoters: "Votantes",
|
pollVoters: "Votantes",
|
||||||
pollComments: "Comentarios",
|
|
||||||
pollStatusLabel: "Estado",
|
pollStatusLabel: "Estado",
|
||||||
pollStatusOpen: "ABIERTA",
|
pollStatusOpen: "ABIERTA",
|
||||||
pollStatusClosed: "CERRADA",
|
pollStatusClosed: "CERRADA",
|
||||||
|
|
@ -1709,9 +1705,22 @@ module.exports = {
|
||||||
blogAllowComments: "Permitir comentarios",
|
blogAllowComments: "Permitir comentarios",
|
||||||
blogCommentsClosed: "La autora ha cerrado los comentarios de este blog.",
|
blogCommentsClosed: "La autora ha cerrado los comentarios de este blog.",
|
||||||
blogNoItems: "No se han encontrado blogs.",
|
blogNoItems: "No se han encontrado blogs.",
|
||||||
viewBlog: "Ver Blog",
|
|
||||||
forumTitle: "Foros",
|
forumTitle: "Foros",
|
||||||
forumCategoryLabel: "Categoría",
|
forumCategoryLabel: "Categoría",
|
||||||
|
forumCatGENERAL: "GENERAL",
|
||||||
|
forumCatPOLITICS: "POLÍTICA",
|
||||||
|
forumCatTECH: "TECNOLOGÍA",
|
||||||
|
forumCatSCIENCE: "CIENCIA",
|
||||||
|
forumCatMUSIC: "MÚSICA",
|
||||||
|
forumCatART: "ARTE",
|
||||||
|
forumCatGAMING: "JUEGOS",
|
||||||
|
forumCatBOOKS: "LIBROS",
|
||||||
|
forumCatFILMS: "PELÍCULAS",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFÍA",
|
||||||
|
forumCatSOCIETY: "SOCIEDAD",
|
||||||
|
forumCatPRIVACY: "PRIVACIDAD",
|
||||||
|
forumCatCYBERWARFARE: "CIBERGUERRA",
|
||||||
|
forumCatSURVIVALISM: "SUPERVIVENCIA",
|
||||||
forumTitleLabel: "Título",
|
forumTitleLabel: "Título",
|
||||||
forumTitlePlaceholder: "Título del foro...",
|
forumTitlePlaceholder: "Título del foro...",
|
||||||
forumCreateButton: "Crear foro",
|
forumCreateButton: "Crear foro",
|
||||||
|
|
@ -1770,8 +1779,6 @@ module.exports = {
|
||||||
feedTitle: "Feed",
|
feedTitle: "Feed",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Abrir discusión",
|
feedOpenDiscussion: "Abrir discusión",
|
||||||
feedPostComment: "Publicar comentario",
|
|
||||||
noComments: "Aún no hay comentarios",
|
|
||||||
createFeedTitle: "Crear Feed",
|
createFeedTitle: "Crear Feed",
|
||||||
createFeedButton: "¡Enviar feed!",
|
createFeedButton: "¡Enviar feed!",
|
||||||
feedPlaceholder: "¿Qué está pasando? (máximo 280 caracteres)",
|
feedPlaceholder: "¿Qué está pasando? (máximo 280 caracteres)",
|
||||||
|
|
@ -1982,42 +1989,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Informes en Revisión",
|
reportsUnderReviewSectionTitle: "Informes en Revisión",
|
||||||
reportsResolvedSectionTitle: "Informes Resueltos",
|
reportsResolvedSectionTitle: "Informes Resueltos",
|
||||||
reportsInvalidSectionTitle: "Informes Inválidos",
|
reportsInvalidSectionTitle: "Informes Inválidos",
|
||||||
reportsBugTemplateTitle: 'Datos para reproducir (Bugs)',
|
|
||||||
reportsFeatureTemplateTitle: 'Detalles (Features)',
|
|
||||||
reportsAbuseTemplateTitle: 'Detalles (Abuso)',
|
|
||||||
reportsContentTemplateTitle: 'Detalles (Problemas de Contenido)',
|
|
||||||
reportsStepsToReproduceLabel: 'Pasos para reproducir',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Resultado esperado',
|
|
||||||
reportsExpectedBehaviorPlaceholder: '¿Qué debería pasar?',
|
|
||||||
reportsActualBehaviorLabel: 'Resultado actual',
|
|
||||||
reportsActualBehaviorPlaceholder: '¿Qué pasa realmente?',
|
|
||||||
reportsEnvironmentLabel: 'Entorno',
|
|
||||||
reportsEnvironmentPlaceholder: 'Versión, dispositivo, OS, navegador, configuración, logs, etc.',
|
|
||||||
reportsReproduceRateLabel: 'Frecuencia',
|
|
||||||
reportsReproduceRateAlways: 'Siempre',
|
|
||||||
reportsReproduceRateOften: 'A menudo',
|
|
||||||
reportsReproduceRateSometimes: 'A veces',
|
|
||||||
reportsReproduceRateRarely: 'Rara vez',
|
|
||||||
reportsReproduceRateUnable: 'No se puede reproducir',
|
|
||||||
reportsReproduceRateUnknown: 'No especificado',
|
|
||||||
reportsProblemStatementLabel: 'Problema / necesidad',
|
|
||||||
reportsProblemStatementPlaceholder: '¿Qué problema resuelve esta feature?',
|
|
||||||
reportsUserStoryLabel: 'Historia de habitante',
|
|
||||||
reportsUserStoryPlaceholder: 'Como <habitante>, quiero <acción>, para <beneficio>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Criterios de aceptación',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Dado...\n- Cuando...\n- Entonces...',
|
|
||||||
reportsWhatHappenedLabel: '¿Qué ocurrió?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Describe el incidente con contexto y fechas aproximadas.',
|
|
||||||
reportsReportedUserLabel: 'Habitante / entidad reportada',
|
|
||||||
reportsReportedUserPlaceholder: '@habitante, feed, ID, enlace, etc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Evidencia / enlaces',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Pega enlaces, IDs de mensajes, capturas (si aplica), etc.',
|
|
||||||
reportsContentLocationLabel: 'Dónde está el contenido',
|
|
||||||
reportsContentLocationPlaceholder: 'Enlace, ID, canal, hilo, autoría, etc.',
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Explica el motivo y el impacto.',
|
|
||||||
reportsRequestedActionLabel: 'Acción solicitada',
|
|
||||||
reportsRequestedActionPlaceholder: 'Eliminar, ocultar, marcar, advertir, etc.',
|
|
||||||
tribesTitle: "Tribus",
|
tribesTitle: "Tribus",
|
||||||
tribeFilterAll: "TODAS",
|
tribeFilterAll: "TODAS",
|
||||||
tribeFilterRecent: "RECIENTES",
|
tribeFilterRecent: "RECIENTES",
|
||||||
|
|
@ -2261,6 +2232,7 @@ module.exports = {
|
||||||
voteWellResearched: "Bien investigado",
|
voteWellResearched: "Bien investigado",
|
||||||
voteNeedsSources: "Necesita fuentes",
|
voteNeedsSources: "Necesita fuentes",
|
||||||
voteWrong: "Incorrecto",
|
voteWrong: "Incorrecto",
|
||||||
|
voteOutdated: "Desactualizado",
|
||||||
voteLowQuality: "Baja calidad",
|
voteLowQuality: "Baja calidad",
|
||||||
voteLove: "Amor",
|
voteLove: "Amor",
|
||||||
voteClear: "Claro",
|
voteClear: "Claro",
|
||||||
|
|
@ -2505,7 +2477,6 @@ module.exports = {
|
||||||
shopNoProducts: "Aún no hay productos.",
|
shopNoProducts: "Aún no hay productos.",
|
||||||
shopOutOfStock: "Agotado",
|
shopOutOfStock: "Agotado",
|
||||||
shopBuy: "Comprar",
|
shopBuy: "Comprar",
|
||||||
shopBuyEncryptedNote: "Tus datos de entrega se envían cifrados; solo la dueña de la tienda puede verlos.",
|
|
||||||
shopBuyDeliveryAddress: "Dirección de entrega",
|
shopBuyDeliveryAddress: "Dirección de entrega",
|
||||||
shopBuyContact: "Contacto",
|
shopBuyContact: "Contacto",
|
||||||
shopBuyContactPlaceholder: "correo, teléfono, etc.",
|
shopBuyContactPlaceholder: "correo, teléfono, etc.",
|
||||||
|
|
@ -3022,6 +2993,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Descripción",
|
industryDescriptionLabel: "Descripción",
|
||||||
industryDescriptionPlaceholder: "¿Qué produce esta fábrica?",
|
industryDescriptionPlaceholder: "¿Qué produce esta fábrica?",
|
||||||
industrySector: "Sector",
|
industrySector: "Sector",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Agricultura",
|
||||||
|
industrySector_textile: "Textil",
|
||||||
|
industrySector_energy: "Energía",
|
||||||
|
industrySector_food: "Alimentación",
|
||||||
|
industrySector_construction: "Construcción",
|
||||||
|
industrySector_media: "Medios",
|
||||||
|
industrySector_services: "Servicios",
|
||||||
|
industrySector_other: "Otros",
|
||||||
industryMembershipPolicy: "Política de membresía",
|
industryMembershipPolicy: "Política de membresía",
|
||||||
industryQuorum: "Quórum",
|
industryQuorum: "Quórum",
|
||||||
industryMajority: "Mayoría",
|
industryMajority: "Mayoría",
|
||||||
|
|
@ -3223,6 +3204,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Módulo para gestionar tu contenido favorito.",
|
modulesFavoritesDescription: "Módulo para gestionar tu contenido favorito.",
|
||||||
fileTooLargeTitle: "Archivo demasiado grande",
|
fileTooLargeTitle: "Archivo demasiado grande",
|
||||||
fileTooLargeMessage: "El archivo supera el tamaño máximo permitido (75 MB). Por favor, selecciona un archivo más pequeño.",
|
fileTooLargeMessage: "El archivo supera el tamaño máximo permitido (75 MB). Por favor, selecciona un archivo más pequeño.",
|
||||||
|
fileWrongType: "Este fichero no es del tipo adecuado para esta sección.",
|
||||||
|
mediaLockedAfterOpinions: "Ya tiene opiniones: puedes editar el resto, pero no cambiar lo que se ha valorado.",
|
||||||
|
pdfPrevPage: "Página anterior",
|
||||||
|
pdfNextPage: "Página siguiente",
|
||||||
|
pdfPage: "Página",
|
||||||
|
pdfZoomIn: "Acercar",
|
||||||
|
pdfZoomOut: "Alejar",
|
||||||
|
pdfFullscreen: "Pantalla completa",
|
||||||
|
pdfClose: "Cerrar",
|
||||||
|
pdfRotate: "Girar",
|
||||||
|
contentNotAvailableYet: "Este contenido aún no está disponible.",
|
||||||
|
pdfRetry: "Reintentar",
|
||||||
goBack: "Volver",
|
goBack: "Volver",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Ha ocurrido un error",
|
errorPageTitle: "Ha ocurrido un error",
|
||||||
|
|
@ -3267,7 +3260,6 @@ module.exports = {
|
||||||
invitesFollow: "Seguir",
|
invitesFollow: "Seguir",
|
||||||
invitesUnfollowedInvites: "No federadas",
|
invitesUnfollowedInvites: "No federadas",
|
||||||
invitesNoUnfollowed: "No hay pubs no federados.",
|
invitesNoUnfollowed: "No hay pubs no federados.",
|
||||||
reportsWhyInappropriateLabel: "¿Por qué es inapropiado?",
|
|
||||||
blockchainContentDeleted: "Este contenido ha sido eliminado",
|
blockchainContentDeleted: "Este contenido ha sido eliminado",
|
||||||
visitContent: "Visitar Contenido",
|
visitContent: "Visitar Contenido",
|
||||||
favoriteAdd: "Fijar en Favoritos",
|
favoriteAdd: "Fijar en Favoritos",
|
||||||
|
|
@ -3579,6 +3571,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
podcastTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
||||||
podcastFilterAll: "TODOS",
|
podcastFilterAll: "TODOS",
|
||||||
podcastFilterMine: "MÍOS",
|
podcastFilterMine: "MÍOS",
|
||||||
|
podcastFilterFavorites: "FAVORITOS",
|
||||||
|
podcastEpisodeNumberLabel: "Número de episodio",
|
||||||
podcastFilterRecent: "RECIENTES",
|
podcastFilterRecent: "RECIENTES",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "OYENTES",
|
podcastFilterViewers: "OYENTES",
|
||||||
|
|
@ -3797,7 +3791,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Volver a la página",
|
wikiBackToPage: "Volver a la página",
|
||||||
wikiLinkedChip: "Enlazada",
|
wikiLinkedChip: "Enlazada",
|
||||||
wikiRestoredSummary: "Versión restaurada",
|
wikiRestoredSummary: "Versión restaurada",
|
||||||
wikiExportPdf: "Generar página",
|
|
||||||
wikiPreview: "Vista previa",
|
wikiPreview: "Vista previa",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Crear wiki",
|
tribeWikiCreate: "Crear wiki",
|
||||||
|
|
@ -3865,6 +3858,9 @@ module.exports = {
|
||||||
roomRecStop: "Parar grabación",
|
roomRecStop: "Parar grabación",
|
||||||
roomStreamFile: "Emitir",
|
roomStreamFile: "Emitir",
|
||||||
roomStreamStop: "Parar",
|
roomStreamStop: "Parar",
|
||||||
|
roomScreen: "Pantalla",
|
||||||
|
roomScreenSharing: "Compartiendo pantalla",
|
||||||
|
roomScreenWarning: "Todos en la sala verán tu pantalla completa hasta que pares.",
|
||||||
roomStreaming: "Emitiendo",
|
roomStreaming: "Emitiendo",
|
||||||
liveFullscreen: "Pantalla completa",
|
liveFullscreen: "Pantalla completa",
|
||||||
roomStreamPickTitle: "Elige un vídeo",
|
roomStreamPickTitle: "Elige un vídeo",
|
||||||
|
|
@ -3922,6 +3918,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Ya formas parte. Puedes abrirlo desde su módulo.",
|
inviteAlreadyMember: "Ya formas parte. Puedes abrirlo desde su módulo.",
|
||||||
actionFailed: "No se ha podido hacer. Inténtalo de nuevo.",
|
actionFailed: "No se ha podido hacer. Inténtalo de nuevo.",
|
||||||
publishTooShort: "Tu publicación es demasiado corta.",
|
publishTooShort: "Tu publicación es demasiado corta.",
|
||||||
|
publishEmpty: "Escribe algo o adjunta un fichero.",
|
||||||
walletSendFailed: "No se ha podido enviar el pago: {reason}",
|
walletSendFailed: "No se ha podido enviar el pago: {reason}",
|
||||||
pixeliaCoordsError: "Esas coordenadas están fuera de la cuadrícula.",
|
pixeliaCoordsError: "Esas coordenadas están fuera de la cuadrícula.",
|
||||||
courtsErrTitle: "Hace falta un título.",
|
courtsErrTitle: "Hace falta un título.",
|
||||||
|
|
@ -4434,6 +4431,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Etiquetas",
|
torrentTagsLabel: "Etiquetas",
|
||||||
torrentTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
torrentTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
||||||
torrentSizeLabel: "Tamaño",
|
torrentSizeLabel: "Tamaño",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Ficheros",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Generar torrent",
|
torrentFromContent: "Generar torrent",
|
||||||
torrentFromContentMissing: "El fichero aún no está disponible en este nodo. Inténtalo en un rato.",
|
torrentFromContentMissing: "El fichero aún no está disponible en este nodo. Inténtalo en un rato.",
|
||||||
torrentDownloads: "Descargas",
|
torrentDownloads: "Descargas",
|
||||||
|
|
@ -4519,9 +4519,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Aceptar",
|
inhabitantsPendingAccept: "Aceptar",
|
||||||
inhabitantsPendingReject: "Rechazar",
|
inhabitantsPendingReject: "Rechazar",
|
||||||
tribeSectionGovernance: "GOBIERNO",
|
tribeSectionGovernance: "GOBIERNO",
|
||||||
tribePadInviteRequired: "No tienes acceso al pad. Pide una invitación para acceder al contenido.",
|
|
||||||
tribeChatInviteRequired: "No tienes acceso al chat. Pide una invitación para acceder al contenido.",
|
|
||||||
tribeGovernanceDesc: "Gobierno interno de esta tribu.",
|
|
||||||
tribeGovernanceNoGov: "Sin gobierno activo",
|
tribeGovernanceNoGov: "Sin gobierno activo",
|
||||||
tribeGovernanceNoGovDesc: "Esta tribu aún no tiene gobierno. Propón candidaturas para iniciar el proceso.",
|
tribeGovernanceNoGovDesc: "Esta tribu aún no tiene gobierno. Propón candidaturas para iniciar el proceso.",
|
||||||
tribeGovCardTitle: "Gobierno Actual",
|
tribeGovCardTitle: "Gobierno Actual",
|
||||||
|
|
@ -4578,7 +4575,6 @@ module.exports = {
|
||||||
logsFilterMonth: "ÚLTIMO MES",
|
logsFilterMonth: "ÚLTIMO MES",
|
||||||
logsFilterWeek: "ÚLTIMA SEMANA",
|
logsFilterWeek: "ÚLTIMA SEMANA",
|
||||||
logsFilterToday: "HOY",
|
logsFilterToday: "HOY",
|
||||||
logsComments: "Comentarios",
|
|
||||||
logsAuthorEntries: "Entradas",
|
logsAuthorEntries: "Entradas",
|
||||||
logsCreate: "Crear entrada",
|
logsCreate: "Crear entrada",
|
||||||
logsExport: "Exportar bitácora",
|
logsExport: "Exportar bitácora",
|
||||||
|
|
@ -4600,7 +4596,7 @@ module.exports = {
|
||||||
larpNoHousesMatch: "Ninguna casa coincide con tu búsqueda.",
|
larpNoHousesMatch: "Ninguna casa coincide con tu búsqueda.",
|
||||||
larpSearchPlaceholder: "Buscar en L.A.R.P....",
|
larpSearchPlaceholder: "Buscar en L.A.R.P....",
|
||||||
larpAllHouses: "Casas:",
|
larpAllHouses: "Casas:",
|
||||||
larpFilterRuling: "RIGE",
|
larpFilterRuling: "GOBERNANDO",
|
||||||
larpFilterHouses: "CASAS",
|
larpFilterHouses: "CASAS",
|
||||||
larpFilterRules: "FAQ",
|
larpFilterRules: "FAQ",
|
||||||
larpRulesTitle: "FAQ del L.A.R.P.",
|
larpRulesTitle: "FAQ del L.A.R.P.",
|
||||||
|
|
@ -4657,6 +4653,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local.",
|
settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local.",
|
||||||
settingsLanEnable: "Activar difusión LAN",
|
settingsLanEnable: "Activar difusión LAN",
|
||||||
|
settingsConnectionsTitle: "Conexiones",
|
||||||
|
settingsConnectionsDesc: "Cuántos peers mantiene conectados este nodo a la vez.",
|
||||||
settingsLanTitle: "Difusión LAN",
|
settingsLanTitle: "Difusión LAN",
|
||||||
settingsReplicationTitle: "Replicación",
|
settingsReplicationTitle: "Replicación",
|
||||||
settingsReplicationDesc: "Configura el número de saltos que tu nodo sigue desde tu propio feed al replicar contenido.",
|
settingsReplicationDesc: "Configura el número de saltos que tu nodo sigue desde tu propio feed al replicar contenido.",
|
||||||
|
|
@ -4790,7 +4788,7 @@ module.exports = {
|
||||||
larpGoToHouse: "Ir a tu casa",
|
larpGoToHouse: "Ir a tu casa",
|
||||||
larpBackToAcademia: "Volver a ACADEMIA",
|
larpBackToAcademia: "Volver a ACADEMIA",
|
||||||
larpBadgeYou: "Tú",
|
larpBadgeYou: "Tú",
|
||||||
larpBadgeRuling: "Rige",
|
larpBadgeRuling: "Gobernando",
|
||||||
modulesLarpLabel: "L.A.R.P.",
|
modulesLarpLabel: "L.A.R.P.",
|
||||||
modulesLarpDescription: "Módulo para la capa de juego de rol en vivo.",
|
modulesLarpDescription: "Módulo para la capa de juego de rol en vivo.",
|
||||||
larpProfileQ1: "Cuando te enfrentas a un problema complejo, ¿qué haces primero?",
|
larpProfileQ1: "Cuando te enfrentas a un problema complejo, ¿qué haces primero?",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Nire sarea",
|
graphosBackToMine: "← Nire sarea",
|
||||||
graphosShowing: "Erakusten",
|
graphosShowing: "Erakusten",
|
||||||
graphosYou: "Zu",
|
graphosYou: "Zu",
|
||||||
graphosTotalNodes: "Nodo guztiak",
|
graphosTotalNodes: "Guztira",
|
||||||
|
onlinePlural: "Linean",
|
||||||
mentions: "Aipamenak",
|
mentions: "Aipamenak",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Zu @aipatzen zaituzten mezu guztiak"),
|
strong("Zu @aipatzen zaituzten mezu guztiak"),
|
||||||
|
|
@ -414,6 +415,7 @@ module.exports = {
|
||||||
menuMedia: "Liburutegia",
|
menuMedia: "Liburutegia",
|
||||||
menuTools: "Tresnak",
|
menuTools: "Tresnak",
|
||||||
comment: "Iruzkindu",
|
comment: "Iruzkindu",
|
||||||
|
typeComment: "Iruzkina",
|
||||||
subtopic: "Azpi-gaia",
|
subtopic: "Azpi-gaia",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Laguntza eten",
|
unfollow: "Laguntza eten",
|
||||||
|
|
@ -443,9 +445,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.",
|
"GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -724,10 +723,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Galdera",
|
votesQuestionLabel:"Galdera",
|
||||||
voteStatusOpen:"IREKITA",
|
voteStatusOpen:"IREKITA",
|
||||||
voteStatusClosed:"ITXITA",
|
voteStatusClosed:"ITXITA",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" noiz komentatuta ",
|
|
||||||
a({ href: parentUrl }, " haria"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Komentatu @${authorName}-(r)en bidalketa`],
|
commentTitle: ({ authorName }) => [`Komentatu @${authorName}-(r)en bidalketa`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" azpigaia hemendik sortuta ",
|
" azpigaia hemendik sortuta ",
|
||||||
|
|
@ -862,6 +857,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "PUBak hurrengo zikloan ordaintzen du eta berretsi dezakezun OBU transferentzia bat argitaratzen du.",
|
bankRulesEligibility5: "PUBak hurrengo zikloan ordaintzen du eta berretsi dezakezun OBU transferentzia bat argitaratzen du.",
|
||||||
bankRulesEligibility6: "Helbidea falta bada, feeda gazteegia bada, hilabeteari uko egin badiozu edo PUBak fondorik ez badu, eskaera saltatu eta PUBean erregistratzen da.",
|
bankRulesEligibility6: "Helbidea falta bada, feeda gazteegia bada, hilabeteari uko egin badiozu edo PUBak fondorik ez badu, eskaera saltatu eta PUBean erregistratzen da.",
|
||||||
copyLinkTitle: "Kopiatu esteka",
|
copyLinkTitle: "Kopiatu esteka",
|
||||||
|
cardMenuTitle: "Ekintza gehiago",
|
||||||
|
shareContent: "Partekatu",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "OBUari uko egin",
|
bankRefuseUBI: "OBUari uko egin",
|
||||||
|
|
@ -895,7 +892,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Zure ECOin zorroa ez dago oraindik konfiguratuta.",
|
walletNotConfigured: "Zure ECOin zorroa ez dago oraindik konfiguratuta.",
|
||||||
walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.",
|
walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.",
|
||||||
walletSettingsMobileDescription: "Lotu Oasis gailu honetako ECOin Wallet aplikazioarekin.",
|
walletSettingsMobileDescription: "Lotu Oasis gailu honetako ECOin Wallet aplikazioarekin.",
|
||||||
walletDownloadEcoin: "Deskargatu ECOin!",
|
walletDownloadEcoin: "Deskargatu ECOin",
|
||||||
walletRpcUrl: "RPC URLa",
|
walletRpcUrl: "RPC URLa",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Zenbatekoa oker",
|
invalid_amount: "Zenbatekoa oker",
|
||||||
|
|
@ -948,6 +945,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Eguneratu",
|
bookmarkUpdateButton: "Eguneratu",
|
||||||
bookmarkAddFavoriteButton: "Gogokoetara gehitu",
|
bookmarkAddFavoriteButton: "Gogokoetara gehitu",
|
||||||
bookmarkRemoveFavoriteButton: "Gogokoetatik kendu",
|
bookmarkRemoveFavoriteButton: "Gogokoetatik kendu",
|
||||||
|
bookmarkTitleLabel: "Izenburua",
|
||||||
bookmarkUrlLabel: "Esteka",
|
bookmarkUrlLabel: "Esteka",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Deskribapena",
|
bookmarkDescriptionLabel: "Deskribapena",
|
||||||
|
|
@ -1092,9 +1090,9 @@ module.exports = {
|
||||||
allCVs: "CVak",
|
allCVs: "CVak",
|
||||||
discoverPeople: "Aurkitu bizilagunak zure sarean.",
|
discoverPeople: "Aurkitu bizilagunak zure sarean.",
|
||||||
searchInhabitantsPlaceholder: "Bilatu hemen: Bizilagunak...",
|
searchInhabitantsPlaceholder: "Bilatu hemen: Bizilagunak...",
|
||||||
filterLocation: "IRAGAZI bizilagunak KOKAPENAREN ARABERA …",
|
filterLocation: "Bilatu kokapenaren arabera…",
|
||||||
filterLanguage: "IRAGAZI bizilagunak BY HIZKUNTZAREN ARABERA …",
|
filterLanguage: "Bilatu hizkuntzaren arabera…",
|
||||||
filterSkills: "IRAGAZI bizilagunak BY TREBETASUNEN ARABERA …",
|
filterSkills: "Bilatu trebetasunen arabera…",
|
||||||
cvSkillsCount: "Trebetasunak",
|
cvSkillsCount: "Trebetasunak",
|
||||||
applyFilters: "Ezarri Iragazkiak",
|
applyFilters: "Ezarri Iragazkiak",
|
||||||
locationLabel: "Kokapena",
|
locationLabel: "Kokapena",
|
||||||
|
|
@ -1110,6 +1108,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Jarduera",
|
topactivitySectionTitle: "Top Jarduera",
|
||||||
"TOP INACTIVITYButton": "INAKTIBOENAK",
|
"TOP INACTIVITYButton": "INAKTIBOENAK",
|
||||||
topinactivitySectionTitle: "Biztanle gutxien aktiboak",
|
topinactivitySectionTitle: "Biztanle gutxien aktiboak",
|
||||||
|
contactsSectionTitle: "Kontaktuak",
|
||||||
blockedSectionTitle: "Blokeatuta",
|
blockedSectionTitle: "Blokeatuta",
|
||||||
gallerySectionTitle: "GALERIA",
|
gallerySectionTitle: "GALERIA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1599,9 +1598,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Data Ezezaguna",
|
voteUnknownDate: "Data Ezezaguna",
|
||||||
voteDetailSectionTitle: 'Bozketa xehetasunak',
|
voteDetailSectionTitle: 'Bozketa xehetasunak',
|
||||||
voteCommentsLabel: 'Iruzkinak',
|
voteCommentsLabel: 'Iruzkinak',
|
||||||
voteCommentsForumButton: 'Eztabaida ireki',
|
voteNoCommentsYet: 'Oraindik ez dago iruzkinik.',
|
||||||
voteCommentsSectionTitle: 'Eztabaida irekia',
|
|
||||||
voteNoCommentsYet: 'Oraindik ez dago iruzkinik. Izan zaitez lehen erantzuten.',
|
|
||||||
commentsGoToLast: "Joan azken iruzkinera",
|
commentsGoToLast: "Joan azken iruzkinera",
|
||||||
voteNewCommentPlaceholder: 'Idatzi hemen zure iruzkina…',
|
voteNewCommentPlaceholder: 'Idatzi hemen zure iruzkina…',
|
||||||
voteNewCommentButton: 'Bidali iruzkina',
|
voteNewCommentButton: 'Bidali iruzkina',
|
||||||
|
|
@ -1728,7 +1725,6 @@ module.exports = {
|
||||||
pollVoteButton: "Bozkatu",
|
pollVoteButton: "Bozkatu",
|
||||||
pollChangeVote: "Aldatu Botoa",
|
pollChangeVote: "Aldatu Botoa",
|
||||||
pollVoters: "Bozkatzaileak",
|
pollVoters: "Bozkatzaileak",
|
||||||
pollComments: "Iruzkinak",
|
|
||||||
pollStatusLabel: "Egoera",
|
pollStatusLabel: "Egoera",
|
||||||
pollStatusOpen: "IREKIA",
|
pollStatusOpen: "IREKIA",
|
||||||
pollStatusClosed: "ITXIA",
|
pollStatusClosed: "ITXIA",
|
||||||
|
|
@ -1750,9 +1746,22 @@ module.exports = {
|
||||||
blogAllowComments: "Baimendu iruzkinak",
|
blogAllowComments: "Baimendu iruzkinak",
|
||||||
blogCommentsClosed: "Egileak blog honen iruzkinak itxi ditu.",
|
blogCommentsClosed: "Egileak blog honen iruzkinak itxi ditu.",
|
||||||
blogNoItems: "Ez da blogik aurkitu.",
|
blogNoItems: "Ez da blogik aurkitu.",
|
||||||
viewBlog: "Ikusi Bloga",
|
|
||||||
forumTitle: "Foroak",
|
forumTitle: "Foroak",
|
||||||
forumCategoryLabel: "Kategoria",
|
forumCategoryLabel: "Kategoria",
|
||||||
|
forumCatGENERAL: "OROKORRA",
|
||||||
|
forumCatPOLITICS: "POLITIKA",
|
||||||
|
forumCatTECH: "TEKNOLOGIA",
|
||||||
|
forumCatSCIENCE: "ZIENTZIA",
|
||||||
|
forumCatMUSIC: "MUSIKA",
|
||||||
|
forumCatART: "ARTEA",
|
||||||
|
forumCatGAMING: "JOKOAK",
|
||||||
|
forumCatBOOKS: "LIBURUAK",
|
||||||
|
forumCatFILMS: "FILMAK",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFIA",
|
||||||
|
forumCatSOCIETY: "GIZARTEA",
|
||||||
|
forumCatPRIVACY: "PRIBATUTASUNA",
|
||||||
|
forumCatCYBERWARFARE: "ZIBERGERRA",
|
||||||
|
forumCatSURVIVALISM: "BIZIRAUPENA",
|
||||||
forumTitleLabel: "Izenburua",
|
forumTitleLabel: "Izenburua",
|
||||||
forumTitlePlaceholder: "Foroaren izenburua...",
|
forumTitlePlaceholder: "Foroaren izenburua...",
|
||||||
forumCreateButton: "Sortu foroa",
|
forumCreateButton: "Sortu foroa",
|
||||||
|
|
@ -1811,8 +1820,6 @@ module.exports = {
|
||||||
feedTitle: "Jarioa",
|
feedTitle: "Jarioa",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Eztabaida ireki",
|
feedOpenDiscussion: "Eztabaida ireki",
|
||||||
feedPostComment: "Iruzkina argitaratu",
|
|
||||||
noComments: "Oraindik iruzkinik ez",
|
|
||||||
createFeedTitle: "Sortu Jarioa",
|
createFeedTitle: "Sortu Jarioa",
|
||||||
createFeedButton: "Bidali Jarioa!",
|
createFeedButton: "Bidali Jarioa!",
|
||||||
feedPlaceholder: "Zer berri? (max 280 characters)",
|
feedPlaceholder: "Zer berri? (max 280 characters)",
|
||||||
|
|
@ -1986,42 +1993,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Txostenak Berrikuspenean",
|
reportsUnderReviewSectionTitle: "Txostenak Berrikuspenean",
|
||||||
reportsResolvedSectionTitle: "Zuzendutako Txostenak",
|
reportsResolvedSectionTitle: "Zuzendutako Txostenak",
|
||||||
reportsInvalidSectionTitle: "Txosten Okerrak",
|
reportsInvalidSectionTitle: "Txosten Okerrak",
|
||||||
reportsBugTemplateTitle: 'Erreprodukzio xehetasunak (Bug-ak)',
|
|
||||||
reportsFeatureTemplateTitle: 'Xehetasunak (Ezaugarriak)',
|
|
||||||
reportsAbuseTemplateTitle: 'Xehetasunak (Abusua)',
|
|
||||||
reportsContentTemplateTitle: 'Xehetasunak (Arazoak Edukiarekin)',
|
|
||||||
reportsStepsToReproduceLabel: 'Erreproduzitzeko urratsak',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Espero den emaitza',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'Zer gertatu beharko litzateke?',
|
|
||||||
reportsActualBehaviorLabel: 'Benetako emaitza',
|
|
||||||
reportsActualBehaviorPlaceholder: 'Zer gertatzen da benetan?',
|
|
||||||
reportsEnvironmentLabel: 'Ingurunea',
|
|
||||||
reportsEnvironmentPlaceholder: 'Bertsioa, gailua, OS, nabigatzailea, ezarpenak, log-ak, etab.',
|
|
||||||
reportsReproduceRateLabel: 'Maiztasuna',
|
|
||||||
reportsReproduceRateAlways: 'Beti',
|
|
||||||
reportsReproduceRateOften: 'Askotan',
|
|
||||||
reportsReproduceRateSometimes: 'Batzuetan',
|
|
||||||
reportsReproduceRateRarely: 'Gutxitan',
|
|
||||||
reportsReproduceRateUnable: 'Ezin da erreproduzitu',
|
|
||||||
reportsReproduceRateUnknown: 'Zehaztu gabe',
|
|
||||||
reportsProblemStatementLabel: 'Arazoa / beharra',
|
|
||||||
reportsProblemStatementPlaceholder: 'Zein arazo konpontzen du ezaugarri honek?',
|
|
||||||
reportsUserStoryLabel: 'User story',
|
|
||||||
reportsUserStoryPlaceholder: '<erabiltzaile> naizenez, <ekintza> nahi dut, <onura> lortzeko.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Onarpen-irizpideak',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Emanda...\n- Noiz...\n- Orduan...',
|
|
||||||
reportsWhatHappenedLabel: 'Zer gertatu da?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Deskribatu gertakaria testuinguruarekin eta gutxi gorabeherako datekin.',
|
|
||||||
reportsReportedUserLabel: 'Salatutako erabiltzailea / entitatea',
|
|
||||||
reportsReportedUserPlaceholder: '@erabiltzailea, feed, ID, esteka, etab.',
|
|
||||||
reportsEvidenceLinksLabel: 'Frogak / estekak',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Itsatsi estekak, mezu ID-ak, pantaila-argazkiak (bada), etab.',
|
|
||||||
reportsContentLocationLabel: 'Non dago edukia',
|
|
||||||
reportsContentLocationPlaceholder: 'Esteka, ID, kanala, haria, egilea, etab.',
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Azaldu arrazoia eta eragina.',
|
|
||||||
reportsRequestedActionLabel: 'Eskatutako ekintza',
|
|
||||||
reportsRequestedActionPlaceholder: 'Kendu, ezkutatu, etiketatu, ohartarazi, etab.',
|
|
||||||
tribesTitle: "Tribuak",
|
tribesTitle: "Tribuak",
|
||||||
tribeFilterAll: "GUZTIAK",
|
tribeFilterAll: "GUZTIAK",
|
||||||
tribeFilterRecent: "BERRIAK",
|
tribeFilterRecent: "BERRIAK",
|
||||||
|
|
@ -2265,6 +2236,7 @@ module.exports = {
|
||||||
voteWellResearched: "Ondo ikertua",
|
voteWellResearched: "Ondo ikertua",
|
||||||
voteNeedsSources: "Iturririk behar du",
|
voteNeedsSources: "Iturririk behar du",
|
||||||
voteWrong: "Okerrak",
|
voteWrong: "Okerrak",
|
||||||
|
voteOutdated: "Zaharkitua",
|
||||||
voteLowQuality: "Behera kalitatea",
|
voteLowQuality: "Behera kalitatea",
|
||||||
voteLove: "Maitasuna",
|
voteLove: "Maitasuna",
|
||||||
voteClear: "Argi",
|
voteClear: "Argi",
|
||||||
|
|
@ -2510,7 +2482,6 @@ module.exports = {
|
||||||
shopNoProducts: "Oraindik produkturik ez.",
|
shopNoProducts: "Oraindik produkturik ez.",
|
||||||
shopOutOfStock: "Agortuta",
|
shopOutOfStock: "Agortuta",
|
||||||
shopBuy: "Erosi",
|
shopBuy: "Erosi",
|
||||||
shopBuyEncryptedNote: "Zure entrega datuak zifratuta bidaltzen dira, dendaren jabeak soilik ikus ditzake.",
|
|
||||||
shopBuyDeliveryAddress: "Entrega helbidea",
|
shopBuyDeliveryAddress: "Entrega helbidea",
|
||||||
shopBuyContact: "Kontaktua",
|
shopBuyContact: "Kontaktua",
|
||||||
shopBuyContactPlaceholder: "e-posta, telefonoa, etab.",
|
shopBuyContactPlaceholder: "e-posta, telefonoa, etab.",
|
||||||
|
|
@ -3026,6 +2997,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Deskribapena",
|
industryDescriptionLabel: "Deskribapena",
|
||||||
industryDescriptionPlaceholder: "Zer ekoizten du fabrika honek?",
|
industryDescriptionPlaceholder: "Zer ekoizten du fabrika honek?",
|
||||||
industrySector: "Sektorea",
|
industrySector: "Sektorea",
|
||||||
|
industrySector_software: "Softwarea",
|
||||||
|
industrySector_hardware: "Hardwarea",
|
||||||
|
industrySector_agriculture: "Nekazaritza",
|
||||||
|
industrySector_textile: "Ehungintza",
|
||||||
|
industrySector_energy: "Energia",
|
||||||
|
industrySector_food: "Elikadura",
|
||||||
|
industrySector_construction: "Eraikuntza",
|
||||||
|
industrySector_media: "Hedabideak",
|
||||||
|
industrySector_services: "Zerbitzuak",
|
||||||
|
industrySector_other: "Besteak",
|
||||||
industryMembershipPolicy: "Kidetza politika",
|
industryMembershipPolicy: "Kidetza politika",
|
||||||
industryQuorum: "Quoruma",
|
industryQuorum: "Quoruma",
|
||||||
industryMajority: "Gehiengoa",
|
industryMajority: "Gehiengoa",
|
||||||
|
|
@ -3227,6 +3208,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Zure gogoko edukia kudeatzeko modulua.",
|
modulesFavoritesDescription: "Zure gogoko edukia kudeatzeko modulua.",
|
||||||
fileTooLargeTitle: "Fitxategia handiegia",
|
fileTooLargeTitle: "Fitxategia handiegia",
|
||||||
fileTooLargeMessage: "Fitxategiak onartutako gehienezko tamaina gainditzen du (75 MB). Mesedez, hautatu fitxategi txikiago bat.",
|
fileTooLargeMessage: "Fitxategiak onartutako gehienezko tamaina gainditzen du (75 MB). Mesedez, hautatu fitxategi txikiago bat.",
|
||||||
|
fileWrongType: "Fitxategi hau ez da atal honetarako mota egokikoa.",
|
||||||
|
mediaLockedAfterOpinions: "Iritziak ditu dagoeneko: gainerakoa edita dezakezu, baina ez baloratutakoa aldatu.",
|
||||||
|
pdfPrevPage: "Aurreko orria",
|
||||||
|
pdfNextPage: "Hurrengo orria",
|
||||||
|
pdfPage: "Orria",
|
||||||
|
pdfZoomIn: "Hurbildu",
|
||||||
|
pdfZoomOut: "Urrundu",
|
||||||
|
pdfFullscreen: "Pantaila osoa",
|
||||||
|
pdfClose: "Itxi",
|
||||||
|
pdfRotate: "Biratu",
|
||||||
|
contentNotAvailableYet: "Eduki hau ez dago oraindik eskuragarri.",
|
||||||
|
pdfRetry: "Saiatu berriro",
|
||||||
goBack: "Itzuli",
|
goBack: "Itzuli",
|
||||||
errorDismiss: "Ados",
|
errorDismiss: "Ados",
|
||||||
errorPageTitle: "Zerbait gaizki joan da",
|
errorPageTitle: "Zerbait gaizki joan da",
|
||||||
|
|
@ -3268,7 +3261,6 @@ module.exports = {
|
||||||
statsCarbonTombstone: "Tombstoning-aren aztarna",
|
statsCarbonTombstone: "Tombstoning-aren aztarna",
|
||||||
uploadMedia: "Multimedia igo (max: 75MB)",
|
uploadMedia: "Multimedia igo (max: 75MB)",
|
||||||
|
|
||||||
reportsWhyInappropriateLabel: "Zergatik da desegokia?",
|
|
||||||
visitContent: "Bisitatu Edukia",
|
visitContent: "Bisitatu Edukia",
|
||||||
favoriteAdd: "Ainguratu Gogokoetan",
|
favoriteAdd: "Ainguratu Gogokoetan",
|
||||||
pmContentTooltip: "Bidali Mezu Pribatua",
|
pmContentTooltip: "Bidali Mezu Pribatua",
|
||||||
|
|
@ -3579,6 +3571,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Idatzi etiketak komaz bereizita",
|
podcastTagsPlaceholder: "Idatzi etiketak komaz bereizita",
|
||||||
podcastFilterAll: "GUZTIAK",
|
podcastFilterAll: "GUZTIAK",
|
||||||
podcastFilterMine: "NIREAK",
|
podcastFilterMine: "NIREAK",
|
||||||
|
podcastFilterFavorites: "GOGOKOAK",
|
||||||
|
podcastEpisodeNumberLabel: "Atal zenbakia",
|
||||||
podcastFilterRecent: "AZKENAK",
|
podcastFilterRecent: "AZKENAK",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "ENTZULEAK",
|
podcastFilterViewers: "ENTZULEAK",
|
||||||
|
|
@ -3797,7 +3791,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Itzuli orrira",
|
wikiBackToPage: "Itzuli orrira",
|
||||||
wikiLinkedChip: "Lotuta",
|
wikiLinkedChip: "Lotuta",
|
||||||
wikiRestoredSummary: "Berreskuratutako bertsioa",
|
wikiRestoredSummary: "Berreskuratutako bertsioa",
|
||||||
wikiExportPdf: "Orria sortu",
|
|
||||||
wikiPreview: "Aurrebista",
|
wikiPreview: "Aurrebista",
|
||||||
tribeSectionWiki: "WIKIA",
|
tribeSectionWiki: "WIKIA",
|
||||||
tribeWikiCreate: "Wikia sortu",
|
tribeWikiCreate: "Wikia sortu",
|
||||||
|
|
@ -3865,6 +3858,9 @@ module.exports = {
|
||||||
roomRecStop: "Grabazioa gelditu",
|
roomRecStop: "Grabazioa gelditu",
|
||||||
roomStreamFile: "Igorri",
|
roomStreamFile: "Igorri",
|
||||||
roomStreamStop: "Gelditu",
|
roomStreamStop: "Gelditu",
|
||||||
|
roomScreen: "Pantaila",
|
||||||
|
roomScreenSharing: "Pantaila partekatzen",
|
||||||
|
roomScreenWarning: "Gelako guztiek zure pantaila osoa ikusiko dute gelditu arte.",
|
||||||
roomStreaming: "Igortzen",
|
roomStreaming: "Igortzen",
|
||||||
liveFullscreen: "Pantaila osoa",
|
liveFullscreen: "Pantaila osoa",
|
||||||
roomStreamPickTitle: "Aukeratu bideo bat",
|
roomStreamPickTitle: "Aukeratu bideo bat",
|
||||||
|
|
@ -3922,6 +3918,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Dagoeneko bertan zaude. Bere modulutik ireki dezakezu.",
|
inviteAlreadyMember: "Dagoeneko bertan zaude. Bere modulutik ireki dezakezu.",
|
||||||
actionFailed: "Ezin izan da egin. Saiatu berriro.",
|
actionFailed: "Ezin izan da egin. Saiatu berriro.",
|
||||||
publishTooShort: "Zure argitalpena laburregia da.",
|
publishTooShort: "Zure argitalpena laburregia da.",
|
||||||
|
publishEmpty: "Idatzi zerbait edo erantsi fitxategi bat.",
|
||||||
walletSendFailed: "Ezin izan da ordainketa bidali: {reason}",
|
walletSendFailed: "Ezin izan da ordainketa bidali: {reason}",
|
||||||
pixeliaCoordsError: "Koordenatu horiek saretik kanpo daude.",
|
pixeliaCoordsError: "Koordenatu horiek saretik kanpo daude.",
|
||||||
courtsErrTitle: "Izenburu bat behar da.",
|
courtsErrTitle: "Izenburu bat behar da.",
|
||||||
|
|
@ -4433,6 +4430,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Etiketak",
|
torrentTagsLabel: "Etiketak",
|
||||||
torrentTagsPlaceholder: "Idatzi etiketak komaz bereizita",
|
torrentTagsPlaceholder: "Idatzi etiketak komaz bereizita",
|
||||||
torrentSizeLabel: "Tamaina",
|
torrentSizeLabel: "Tamaina",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Fitxategiak",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Torrenta sortu",
|
torrentFromContent: "Torrenta sortu",
|
||||||
torrentFromContentMissing: "Fitxategia oraindik ez dago nodo honetan eskuragarri. Saiatu geroago.",
|
torrentFromContentMissing: "Fitxategia oraindik ez dago nodo honetan eskuragarri. Saiatu geroago.",
|
||||||
torrentDownloads: "Deskargak",
|
torrentDownloads: "Deskargak",
|
||||||
|
|
@ -4518,9 +4518,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Onartu",
|
inhabitantsPendingAccept: "Onartu",
|
||||||
inhabitantsPendingReject: "Ukatu",
|
inhabitantsPendingReject: "Ukatu",
|
||||||
tribeSectionGovernance: "GOBERNANTZA",
|
tribeSectionGovernance: "GOBERNANTZA",
|
||||||
tribePadInviteRequired: "Ez duzu pada atzipena. Eskatu gonbidapena.",
|
|
||||||
tribeChatInviteRequired: "Ez duzu txata atzipena. Eskatu gonbidapena.",
|
|
||||||
tribeGovernanceDesc: "Tribu honen barne gobernantza.",
|
|
||||||
tribeGovernanceNoGov: "Ez dago gobernu aktiborik",
|
tribeGovernanceNoGov: "Ez dago gobernu aktiborik",
|
||||||
tribeGovernanceNoGovDesc: "Tribu honek ez du oraindik gobernurik aukeratu.",
|
tribeGovernanceNoGovDesc: "Tribu honek ez du oraindik gobernurik aukeratu.",
|
||||||
tribeGovCardTitle: "Egungo Gobernua",
|
tribeGovCardTitle: "Egungo Gobernua",
|
||||||
|
|
@ -4577,7 +4574,6 @@ module.exports = {
|
||||||
logsFilterMonth: "AZKEN HILABETEA",
|
logsFilterMonth: "AZKEN HILABETEA",
|
||||||
logsFilterWeek: "AZKEN ASTEA",
|
logsFilterWeek: "AZKEN ASTEA",
|
||||||
logsFilterToday: "GAUR",
|
logsFilterToday: "GAUR",
|
||||||
logsComments: "Iruzkinak",
|
|
||||||
logsAuthorEntries: "Sarrerak",
|
logsAuthorEntries: "Sarrerak",
|
||||||
logsCreate: "Sarrera sortu",
|
logsCreate: "Sarrera sortu",
|
||||||
logsExport: "Egunkaria esportatu",
|
logsExport: "Egunkaria esportatu",
|
||||||
|
|
@ -4736,6 +4732,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "WILL proba",
|
larpWillTestTitle: "WILL proba",
|
||||||
settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei.",
|
settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei.",
|
||||||
settingsLanEnable: "Gaitu LAN igorpena",
|
settingsLanEnable: "Gaitu LAN igorpena",
|
||||||
|
settingsConnectionsTitle: "Konexioak",
|
||||||
|
settingsConnectionsDesc: "Nodo honek aldi berean konektatuta mantentzen dituen peer kopurua.",
|
||||||
settingsLanTitle: "LAN igorpena",
|
settingsLanTitle: "LAN igorpena",
|
||||||
settingsReplicationTitle: "Erreplikazioa",
|
settingsReplicationTitle: "Erreplikazioa",
|
||||||
settingsReplicationDesc: "Konfiguratu zenbat jauzi jarraitu behar dituen zure kideak zure jariotik abiatuz edukia erreplikatzean.",
|
settingsReplicationDesc: "Konfiguratu zenbat jauzi jarraitu behar dituen zure kideak zure jariotik abiatuz edukia erreplikatzean.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Mon réseau",
|
graphosBackToMine: "← Mon réseau",
|
||||||
graphosShowing: "Affichage",
|
graphosShowing: "Affichage",
|
||||||
graphosYou: "Toi",
|
graphosYou: "Toi",
|
||||||
graphosTotalNodes: "Nœuds totaux",
|
graphosTotalNodes: "Totaux",
|
||||||
|
onlinePlural: "En ligne",
|
||||||
mentions: "Mentions",
|
mentions: "Mentions",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Chaque message qui vous @mentionne"),
|
strong("Chaque message qui vous @mentionne"),
|
||||||
|
|
@ -412,6 +413,7 @@ module.exports = {
|
||||||
menuMedia: "Bibliothèque",
|
menuMedia: "Bibliothèque",
|
||||||
menuTools: "Outils",
|
menuTools: "Outils",
|
||||||
comment: "Commenter",
|
comment: "Commenter",
|
||||||
|
typeComment: "Commentaire",
|
||||||
subtopic: "Sous-sujet",
|
subtopic: "Sous-sujet",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Ne plus soutenir",
|
unfollow: "Ne plus soutenir",
|
||||||
|
|
@ -441,9 +443,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !",
|
"RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -718,10 +717,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Question",
|
votesQuestionLabel:"Question",
|
||||||
voteStatusOpen:"OUVERT",
|
voteStatusOpen:"OUVERT",
|
||||||
voteStatusClosed:"FERMÉ",
|
voteStatusClosed:"FERMÉ",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" a commenté dans ",
|
|
||||||
a({ href: parentUrl }, " fil"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`A commenté sur le post de @${authorName}`],
|
commentTitle: ({ authorName }) => [`A commenté sur le post de @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" a créé un sous-sujet depuis ",
|
" a créé un sous-sujet depuis ",
|
||||||
|
|
@ -856,6 +851,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "Le PUB paie à son prochain cycle et publie un transfert RBU que vous pouvez confirmer.",
|
bankRulesEligibility5: "Le PUB paie à son prochain cycle et publie un transfert RBU que vous pouvez confirmer.",
|
||||||
bankRulesEligibility6: "Sans adresse, avec un feed trop jeune, un mois refusé ou un PUB sans fonds, la réclamation est ignorée et journalisée sur le PUB.",
|
bankRulesEligibility6: "Sans adresse, avec un feed trop jeune, un mois refusé ou un PUB sans fonds, la réclamation est ignorée et journalisée sur le PUB.",
|
||||||
copyLinkTitle: "Copier le lien",
|
copyLinkTitle: "Copier le lien",
|
||||||
|
cardMenuTitle: "Plus d'actions",
|
||||||
|
shareContent: "Partager",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Refuser le RBU",
|
bankRefuseUBI: "Refuser le RBU",
|
||||||
|
|
@ -889,7 +886,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Votre portefeuille ECOin n'est pas encore configuré.",
|
walletNotConfigured: "Votre portefeuille ECOin n'est pas encore configuré.",
|
||||||
walletSettingsDescription: "Intégrer Oasis avec votre portefeuille ECOin.",
|
walletSettingsDescription: "Intégrer Oasis avec votre portefeuille ECOin.",
|
||||||
walletSettingsMobileDescription: "Associez Oasis à l’application ECOin Wallet de cet appareil.",
|
walletSettingsMobileDescription: "Associez Oasis à l’application ECOin Wallet de cet appareil.",
|
||||||
walletDownloadEcoin: "Télécharger ECOin !",
|
walletDownloadEcoin: "Télécharger ECOin",
|
||||||
walletRpcUrl: "URL du RPC",
|
walletRpcUrl: "URL du RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Montant invalide",
|
invalid_amount: "Montant invalide",
|
||||||
|
|
@ -942,6 +939,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Mettre à jour",
|
bookmarkUpdateButton: "Mettre à jour",
|
||||||
bookmarkAddFavoriteButton: "Ajouter aux favoris",
|
bookmarkAddFavoriteButton: "Ajouter aux favoris",
|
||||||
bookmarkRemoveFavoriteButton: "Retirer des favoris",
|
bookmarkRemoveFavoriteButton: "Retirer des favoris",
|
||||||
|
bookmarkTitleLabel: "Titre",
|
||||||
bookmarkUrlLabel: "Lien",
|
bookmarkUrlLabel: "Lien",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Description",
|
bookmarkDescriptionLabel: "Description",
|
||||||
|
|
@ -1086,9 +1084,9 @@ module.exports = {
|
||||||
allCVs: "CV",
|
allCVs: "CV",
|
||||||
discoverPeople: "Découvrez des habitants dans votre réseau.",
|
discoverPeople: "Découvrez des habitants dans votre réseau.",
|
||||||
searchInhabitantsPlaceholder: "Rechercher dans Habitants...",
|
searchInhabitantsPlaceholder: "Rechercher dans Habitants...",
|
||||||
filterLocation: "FILTRER les habitants PAR LOCALISATION …",
|
filterLocation: "Rechercher par localisation…",
|
||||||
filterLanguage: "FILTRER les habitants PAR LANGUE …",
|
filterLanguage: "Rechercher par langue…",
|
||||||
filterSkills: "FILTRER les habitants PAR COMPÉTENCES …",
|
filterSkills: "Rechercher par compétences…",
|
||||||
cvSkillsCount: "Compétences",
|
cvSkillsCount: "Compétences",
|
||||||
applyFilters: "Appliquer les filtres",
|
applyFilters: "Appliquer les filtres",
|
||||||
locationLabel: "Localisation",
|
locationLabel: "Localisation",
|
||||||
|
|
@ -1104,6 +1102,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top activité",
|
topactivitySectionTitle: "Top activité",
|
||||||
"TOP INACTIVITYButton": "PLUS INACTIFS",
|
"TOP INACTIVITYButton": "PLUS INACTIFS",
|
||||||
topinactivitySectionTitle: "Habitants les moins actifs",
|
topinactivitySectionTitle: "Habitants les moins actifs",
|
||||||
|
contactsSectionTitle: "Contacts",
|
||||||
blockedSectionTitle: "Bloqués",
|
blockedSectionTitle: "Bloqués",
|
||||||
gallerySectionTitle: "GALERIE",
|
gallerySectionTitle: "GALERIE",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1557,9 +1556,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Date inconnue",
|
voteUnknownDate: "Date inconnue",
|
||||||
voteDetailSectionTitle: 'Détails du vote',
|
voteDetailSectionTitle: 'Détails du vote',
|
||||||
voteCommentsLabel: 'Commentaires',
|
voteCommentsLabel: 'Commentaires',
|
||||||
voteCommentsForumButton: 'Ouvrir la discussion',
|
voteNoCommentsYet: 'Il n’y a pas encore de commentaires.',
|
||||||
voteCommentsSectionTitle: 'Discussion ouverte',
|
|
||||||
voteNoCommentsYet: 'Il n’y a pas encore de commentaires. Soyez le premier à répondre.',
|
|
||||||
commentsGoToLast: "Aller au dernier commentaire",
|
commentsGoToLast: "Aller au dernier commentaire",
|
||||||
voteNewCommentPlaceholder: 'Écrivez votre commentaire ici…',
|
voteNewCommentPlaceholder: 'Écrivez votre commentaire ici…',
|
||||||
voteNewCommentButton: 'Publier le commentaire',
|
voteNewCommentButton: 'Publier le commentaire',
|
||||||
|
|
@ -1686,7 +1683,6 @@ module.exports = {
|
||||||
pollVoteButton: "Voter",
|
pollVoteButton: "Voter",
|
||||||
pollChangeVote: "Changer de Vote",
|
pollChangeVote: "Changer de Vote",
|
||||||
pollVoters: "Votants",
|
pollVoters: "Votants",
|
||||||
pollComments: "Commentaires",
|
|
||||||
pollStatusLabel: "Statut",
|
pollStatusLabel: "Statut",
|
||||||
pollStatusOpen: "OUVERT",
|
pollStatusOpen: "OUVERT",
|
||||||
pollStatusClosed: "FERMÉ",
|
pollStatusClosed: "FERMÉ",
|
||||||
|
|
@ -1708,9 +1704,22 @@ module.exports = {
|
||||||
blogAllowComments: "Autoriser les commentaires",
|
blogAllowComments: "Autoriser les commentaires",
|
||||||
blogCommentsClosed: "L'auteur a fermé les commentaires de ce blog.",
|
blogCommentsClosed: "L'auteur a fermé les commentaires de ce blog.",
|
||||||
blogNoItems: "Aucun blog trouvé.",
|
blogNoItems: "Aucun blog trouvé.",
|
||||||
viewBlog: "Voir le Blog",
|
|
||||||
forumTitle: "Forums",
|
forumTitle: "Forums",
|
||||||
forumCategoryLabel: "Catégorie",
|
forumCategoryLabel: "Catégorie",
|
||||||
|
forumCatGENERAL: "GÉNÉRAL",
|
||||||
|
forumCatPOLITICS: "POLITIQUE",
|
||||||
|
forumCatTECH: "TECHNOLOGIE",
|
||||||
|
forumCatSCIENCE: "SCIENCE",
|
||||||
|
forumCatMUSIC: "MUSIQUE",
|
||||||
|
forumCatART: "ART",
|
||||||
|
forumCatGAMING: "JEUX",
|
||||||
|
forumCatBOOKS: "LIVRES",
|
||||||
|
forumCatFILMS: "FILMS",
|
||||||
|
forumCatPHILOSOPHY: "PHILOSOPHIE",
|
||||||
|
forumCatSOCIETY: "SOCIÉTÉ",
|
||||||
|
forumCatPRIVACY: "VIE PRIVÉE",
|
||||||
|
forumCatCYBERWARFARE: "CYBERGUERRE",
|
||||||
|
forumCatSURVIVALISM: "SURVIVALISME",
|
||||||
forumTitleLabel: "Titre",
|
forumTitleLabel: "Titre",
|
||||||
forumTitlePlaceholder: "Titre du forum...",
|
forumTitlePlaceholder: "Titre du forum...",
|
||||||
forumCreateButton: "Créer un forum",
|
forumCreateButton: "Créer un forum",
|
||||||
|
|
@ -1769,8 +1778,6 @@ module.exports = {
|
||||||
feedTitle: "Fil",
|
feedTitle: "Fil",
|
||||||
feedDetailTitle: "Fil",
|
feedDetailTitle: "Fil",
|
||||||
feedOpenDiscussion: "Ouvrir la discussion",
|
feedOpenDiscussion: "Ouvrir la discussion",
|
||||||
feedPostComment: "Publier un commentaire",
|
|
||||||
noComments: "Pas encore de commentaires",
|
|
||||||
createFeedTitle: "Créer un fil",
|
createFeedTitle: "Créer un fil",
|
||||||
createFeedButton: "Envoyer le fil !",
|
createFeedButton: "Envoyer le fil !",
|
||||||
feedPlaceholder: "Que se passe-t-il ? (maximum 280 caractères)",
|
feedPlaceholder: "Que se passe-t-il ? (maximum 280 caractères)",
|
||||||
|
|
@ -1981,36 +1988,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Rapports en révision",
|
reportsUnderReviewSectionTitle: "Rapports en révision",
|
||||||
reportsResolvedSectionTitle: "Rapports résolus",
|
reportsResolvedSectionTitle: "Rapports résolus",
|
||||||
reportsInvalidSectionTitle: "Rapports invalides",
|
reportsInvalidSectionTitle: "Rapports invalides",
|
||||||
reportsBugTemplateTitle: 'Détails de reproduction (Bugs)',
|
|
||||||
reportsFeatureTemplateTitle: 'Détails (Fonctionnalités)',
|
|
||||||
reportsAbuseTemplateTitle: 'Détails (Abus)',
|
|
||||||
reportsContentTemplateTitle: 'Détails (Problèmes de Contenu)',
|
|
||||||
reportsStepsToReproduceLabel: 'Étapes pour reproduire',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Résultat attendu',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'Que devrait-il se passer ?',
|
|
||||||
reportsActualBehaviorLabel: 'Résultat obtenu',
|
|
||||||
reportsEnvironmentLabel: 'Environnement',
|
|
||||||
reportsEnvironmentPlaceholder: 'Version, appareil, OS, navigateur, paramètres, logs, etc.',
|
|
||||||
reportsReproduceRateLabel: 'Fréquence',
|
|
||||||
reportsReproduceRateAlways: 'Toujours',
|
|
||||||
reportsReproduceRateOften: 'Souvent',
|
|
||||||
reportsReproduceRateSometimes: 'Parfois',
|
|
||||||
reportsReproduceRateRarely: 'Rarement',
|
|
||||||
reportsReproduceRateUnable: 'Impossible à reproduire',
|
|
||||||
reportsReproduceRateUnknown: 'Non précisé',
|
|
||||||
reportsProblemStatementLabel: 'Problème / besoin',
|
|
||||||
reportsProblemStatementPlaceholder: 'Quel problème cette fonctionnalité résout-elle ?',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Étant donné...\n- Quand...\n- Alors...',
|
|
||||||
reportsReportedUserLabel: 'Utilisateur / entité signalé(e)',
|
|
||||||
reportsReportedUserPlaceholder: '@utilisateur, feed, ID, lien, etc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Preuves / liens',
|
|
||||||
reportsContentLocationLabel: 'Où se trouve le contenu',
|
|
||||||
reportsContentLocationPlaceholder: 'Lien, ID, canal, fil, auteur, etc.',
|
|
||||||
reportsWhyInappropriateLabel: "Pourquoi est-ce inapproprié ?",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Expliquez la raison et l’impact.',
|
|
||||||
reportsRequestedActionLabel: 'Action demandée',
|
|
||||||
reportsRequestedActionPlaceholder: 'Supprimer, masquer, étiqueter, avertir, etc.',
|
|
||||||
tribesTitle: "Tribus",
|
tribesTitle: "Tribus",
|
||||||
tribeFilterAll: "TOUTES",
|
tribeFilterAll: "TOUTES",
|
||||||
tribeFilterRecent: "RÉCENTES",
|
tribeFilterRecent: "RÉCENTES",
|
||||||
|
|
@ -2254,6 +2231,7 @@ module.exports = {
|
||||||
voteWellResearched: "Bien recherché",
|
voteWellResearched: "Bien recherché",
|
||||||
voteNeedsSources: "Besoin de sources",
|
voteNeedsSources: "Besoin de sources",
|
||||||
voteWrong: "Faux",
|
voteWrong: "Faux",
|
||||||
|
voteOutdated: "Obsolète",
|
||||||
voteLowQuality: "Faible qualité",
|
voteLowQuality: "Faible qualité",
|
||||||
voteLove: "Amour",
|
voteLove: "Amour",
|
||||||
voteClear: "Clair",
|
voteClear: "Clair",
|
||||||
|
|
@ -2499,7 +2477,6 @@ module.exports = {
|
||||||
shopNoProducts: "Pas encore de produits.",
|
shopNoProducts: "Pas encore de produits.",
|
||||||
shopOutOfStock: "Épuisé",
|
shopOutOfStock: "Épuisé",
|
||||||
shopBuy: "Acheter",
|
shopBuy: "Acheter",
|
||||||
shopBuyEncryptedNote: "Vos coordonnées de livraison sont chiffrées et visibles uniquement par le propriétaire de la boutique.",
|
|
||||||
shopBuyDeliveryAddress: "Adresse de livraison",
|
shopBuyDeliveryAddress: "Adresse de livraison",
|
||||||
shopBuyContact: "Contact",
|
shopBuyContact: "Contact",
|
||||||
shopBuyContactPlaceholder: "e-mail, téléphone, etc.",
|
shopBuyContactPlaceholder: "e-mail, téléphone, etc.",
|
||||||
|
|
@ -3015,6 +2992,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Description",
|
industryDescriptionLabel: "Description",
|
||||||
industryDescriptionPlaceholder: "Que produit cette usine ?",
|
industryDescriptionPlaceholder: "Que produit cette usine ?",
|
||||||
industrySector: "Secteur",
|
industrySector: "Secteur",
|
||||||
|
industrySector_software: "Logiciel",
|
||||||
|
industrySector_hardware: "Matériel",
|
||||||
|
industrySector_agriculture: "Agriculture",
|
||||||
|
industrySector_textile: "Textile",
|
||||||
|
industrySector_energy: "Énergie",
|
||||||
|
industrySector_food: "Alimentation",
|
||||||
|
industrySector_construction: "Construction",
|
||||||
|
industrySector_media: "Médias",
|
||||||
|
industrySector_services: "Services",
|
||||||
|
industrySector_other: "Autre",
|
||||||
industryMembershipPolicy: "Politique d'adhésion",
|
industryMembershipPolicy: "Politique d'adhésion",
|
||||||
industryQuorum: "Quorum",
|
industryQuorum: "Quorum",
|
||||||
industryMajority: "Majorité",
|
industryMajority: "Majorité",
|
||||||
|
|
@ -3216,6 +3203,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Module pour gérer votre contenu favori.",
|
modulesFavoritesDescription: "Module pour gérer votre contenu favori.",
|
||||||
fileTooLargeTitle: "Fichier trop volumineux",
|
fileTooLargeTitle: "Fichier trop volumineux",
|
||||||
fileTooLargeMessage: "Le fichier dépasse la taille maximale autorisée (50 Mo). Veuillez sélectionner un fichier plus petit.",
|
fileTooLargeMessage: "Le fichier dépasse la taille maximale autorisée (50 Mo). Veuillez sélectionner un fichier plus petit.",
|
||||||
|
fileWrongType: "Ce fichier n'est pas du bon type pour cette section.",
|
||||||
|
mediaLockedAfterOpinions: "Il a déjà des avis : tu peux modifier le reste, mais pas ce qui a été évalué.",
|
||||||
|
pdfPrevPage: "Page précédente",
|
||||||
|
pdfNextPage: "Page suivante",
|
||||||
|
pdfPage: "Page",
|
||||||
|
pdfZoomIn: "Zoom avant",
|
||||||
|
pdfZoomOut: "Zoom arrière",
|
||||||
|
pdfFullscreen: "Plein écran",
|
||||||
|
pdfClose: "Fermer",
|
||||||
|
pdfRotate: "Pivoter",
|
||||||
|
contentNotAvailableYet: "Ce contenu n’est pas encore disponible.",
|
||||||
|
pdfRetry: "Réessayer",
|
||||||
goBack: "Retour",
|
goBack: "Retour",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Une erreur est survenue",
|
errorPageTitle: "Une erreur est survenue",
|
||||||
|
|
@ -3571,6 +3570,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Saisissez des tags séparés par des virgules",
|
podcastTagsPlaceholder: "Saisissez des tags séparés par des virgules",
|
||||||
podcastFilterAll: "TOUS",
|
podcastFilterAll: "TOUS",
|
||||||
podcastFilterMine: "MIENS",
|
podcastFilterMine: "MIENS",
|
||||||
|
podcastFilterFavorites: "FAVORIS",
|
||||||
|
podcastEpisodeNumberLabel: "Numéro d'épisode",
|
||||||
podcastFilterRecent: "RÉCENTS",
|
podcastFilterRecent: "RÉCENTS",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "AUDITEURS",
|
podcastFilterViewers: "AUDITEURS",
|
||||||
|
|
@ -3789,7 +3790,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Retour à la page",
|
wikiBackToPage: "Retour à la page",
|
||||||
wikiLinkedChip: "Liée",
|
wikiLinkedChip: "Liée",
|
||||||
wikiRestoredSummary: "Version restaurée",
|
wikiRestoredSummary: "Version restaurée",
|
||||||
wikiExportPdf: "Générer la page",
|
|
||||||
wikiPreview: "Aperçu",
|
wikiPreview: "Aperçu",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Créer un wiki",
|
tribeWikiCreate: "Créer un wiki",
|
||||||
|
|
@ -3857,6 +3857,9 @@ module.exports = {
|
||||||
roomRecStop: "Arrêter l'enregistrement",
|
roomRecStop: "Arrêter l'enregistrement",
|
||||||
roomStreamFile: "Diffuser",
|
roomStreamFile: "Diffuser",
|
||||||
roomStreamStop: "Arrêter",
|
roomStreamStop: "Arrêter",
|
||||||
|
roomScreen: "Écran",
|
||||||
|
roomScreenSharing: "Partage d'écran",
|
||||||
|
roomScreenWarning: "Tout le monde dans la salle verra votre écran entier jusqu'à ce que vous arrêtiez.",
|
||||||
roomStreaming: "En diffusion",
|
roomStreaming: "En diffusion",
|
||||||
liveFullscreen: "Plein écran",
|
liveFullscreen: "Plein écran",
|
||||||
roomStreamPickTitle: "Choisir une vidéo",
|
roomStreamPickTitle: "Choisir une vidéo",
|
||||||
|
|
@ -3914,6 +3917,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Vous en faites déjà partie. Vous pouvez l'ouvrir depuis son module.",
|
inviteAlreadyMember: "Vous en faites déjà partie. Vous pouvez l'ouvrir depuis son module.",
|
||||||
actionFailed: "Cela n'a pas pu être fait. Réessayez.",
|
actionFailed: "Cela n'a pas pu être fait. Réessayez.",
|
||||||
publishTooShort: "Votre publication est trop courte.",
|
publishTooShort: "Votre publication est trop courte.",
|
||||||
|
publishEmpty: "Écrivez quelque chose ou joignez un fichier.",
|
||||||
walletSendFailed: "Le paiement n'a pas pu être envoyé : {reason}",
|
walletSendFailed: "Le paiement n'a pas pu être envoyé : {reason}",
|
||||||
pixeliaCoordsError: "Ces coordonnées sont en dehors de la grille.",
|
pixeliaCoordsError: "Ces coordonnées sont en dehors de la grille.",
|
||||||
courtsErrTitle: "Un titre est requis.",
|
courtsErrTitle: "Un titre est requis.",
|
||||||
|
|
@ -4425,6 +4429,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Étiquettes",
|
torrentTagsLabel: "Étiquettes",
|
||||||
torrentTagsPlaceholder: "Saisissez des tags séparés par des virgules",
|
torrentTagsPlaceholder: "Saisissez des tags séparés par des virgules",
|
||||||
torrentSizeLabel: "Taille",
|
torrentSizeLabel: "Taille",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Fichiers",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Générer un torrent",
|
torrentFromContent: "Générer un torrent",
|
||||||
torrentFromContentMissing: "Le fichier n'est pas encore disponible sur ce nœud. Réessayez plus tard.",
|
torrentFromContentMissing: "Le fichier n'est pas encore disponible sur ce nœud. Réessayez plus tard.",
|
||||||
torrentDownloads: "Téléchargements",
|
torrentDownloads: "Téléchargements",
|
||||||
|
|
@ -4510,9 +4517,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Accepter",
|
inhabitantsPendingAccept: "Accepter",
|
||||||
inhabitantsPendingReject: "Refuser",
|
inhabitantsPendingReject: "Refuser",
|
||||||
tribeSectionGovernance: "GOUVERNANCE",
|
tribeSectionGovernance: "GOUVERNANCE",
|
||||||
tribePadInviteRequired: "Vous n'avez pas accès au pad. Demandez une invitation.",
|
|
||||||
tribeChatInviteRequired: "Vous n'avez pas accès au chat. Demandez une invitation.",
|
|
||||||
tribeGovernanceDesc: "Gouvernance interne de cette tribu.",
|
|
||||||
tribeGovernanceNoGov: "Pas de gouvernement actif",
|
tribeGovernanceNoGov: "Pas de gouvernement actif",
|
||||||
tribeGovernanceNoGovDesc: "Cette tribu n'a pas encore de gouvernement.",
|
tribeGovernanceNoGovDesc: "Cette tribu n'a pas encore de gouvernement.",
|
||||||
tribeGovCardTitle: "Gouvernement Actuel",
|
tribeGovCardTitle: "Gouvernement Actuel",
|
||||||
|
|
@ -4569,7 +4573,6 @@ module.exports = {
|
||||||
logsFilterMonth: "MOIS",
|
logsFilterMonth: "MOIS",
|
||||||
logsFilterWeek: "SEMAINE",
|
logsFilterWeek: "SEMAINE",
|
||||||
logsFilterToday: "AUJOURD'HUI",
|
logsFilterToday: "AUJOURD'HUI",
|
||||||
logsComments: "Commentaires",
|
|
||||||
logsAuthorEntries: "Entrées",
|
logsAuthorEntries: "Entrées",
|
||||||
logsCreate: "Créer une entrée",
|
logsCreate: "Créer une entrée",
|
||||||
logsExport: "Exporter le journal",
|
logsExport: "Exporter le journal",
|
||||||
|
|
@ -4632,13 +4635,6 @@ module.exports = {
|
||||||
padAttributionView: "Par autrice",
|
padAttributionView: "Par autrice",
|
||||||
padVersionView: "Voir",
|
padVersionView: "Voir",
|
||||||
parliamentRevocationPublish: "Publier la révocation",
|
parliamentRevocationPublish: "Publier la révocation",
|
||||||
reportsAcceptanceCriteriaLabel: "Critères d'acceptation",
|
|
||||||
reportsActualBehaviorPlaceholder: "Que se passe-t-il réellement ?",
|
|
||||||
reportsEvidenceLinksPlaceholder: "Collez des liens, des identifiants de messages, des captures d'écran (le cas échéant), etc.",
|
|
||||||
reportsUserStoryLabel: "Récit d'habitant",
|
|
||||||
reportsUserStoryPlaceholder: "En tant que <habitant>, je veux <action>, afin de <bénéfice>.",
|
|
||||||
reportsWhatHappenedLabel: "Que s'est-il passé ?",
|
|
||||||
reportsWhatHappenedPlaceholder: "Décrivez l'incident avec le contexte et des dates approximatives.",
|
|
||||||
statsChat: "Chats",
|
statsChat: "Chats",
|
||||||
statsChatMessage: "Messages de chat",
|
statsChatMessage: "Messages de chat",
|
||||||
statsGameScore: "Jeux",
|
statsGameScore: "Jeux",
|
||||||
|
|
@ -4656,6 +4652,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local.",
|
settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local.",
|
||||||
settingsLanEnable: "Activer la diffusion LAN",
|
settingsLanEnable: "Activer la diffusion LAN",
|
||||||
|
settingsConnectionsTitle: "Connexions",
|
||||||
|
settingsConnectionsDesc: "Nombre de pairs que ce nœud garde connectés en même temps.",
|
||||||
settingsLanTitle: "Diffusion LAN",
|
settingsLanTitle: "Diffusion LAN",
|
||||||
settingsReplicationTitle: "Réplication",
|
settingsReplicationTitle: "Réplication",
|
||||||
settingsReplicationDesc: "Configure le nombre de sauts que ton pair suit à partir de ton propre flux lors de la réplication du contenu.",
|
settingsReplicationDesc: "Configure le nombre de sauts que ton pair suit à partir de ton propre flux lors de la réplication du contenu.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← A miña rede",
|
graphosBackToMine: "← A miña rede",
|
||||||
graphosShowing: "Amosando",
|
graphosShowing: "Amosando",
|
||||||
graphosYou: "Ti",
|
graphosYou: "Ti",
|
||||||
graphosTotalNodes: "Nodos totais",
|
graphosTotalNodes: "Totais",
|
||||||
|
onlinePlural: "Conectados",
|
||||||
mentions: "Mencións",
|
mentions: "Mencións",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Todas as mensaxes que te @mencionan"),
|
strong("Todas as mensaxes que te @mencionan"),
|
||||||
|
|
@ -412,6 +413,7 @@ module.exports = {
|
||||||
menuMedia: "Biblioteca",
|
menuMedia: "Biblioteca",
|
||||||
menuTools: "Ferramentas",
|
menuTools: "Ferramentas",
|
||||||
comment: "Comentar",
|
comment: "Comentar",
|
||||||
|
typeComment: "Comentario",
|
||||||
subtopic: "Subtema",
|
subtopic: "Subtema",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Deixar de apoiar",
|
unfollow: "Deixar de apoiar",
|
||||||
|
|
@ -441,9 +443,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!",
|
"LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -718,10 +717,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Pregunta",
|
votesQuestionLabel:"Pregunta",
|
||||||
voteStatusOpen:"ABERTO",
|
voteStatusOpen:"ABERTO",
|
||||||
voteStatusClosed:"PECHADO",
|
voteStatusClosed:"PECHADO",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" comentou en ",
|
|
||||||
a({ href: parentUrl }, " fío"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Comentou no post de @${authorName}`],
|
commentTitle: ({ authorName }) => [`Comentou no post de @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" creou un subtema desde ",
|
" creou un subtema desde ",
|
||||||
|
|
@ -856,6 +851,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "O PUB paga no seu seguinte ciclo e publica un transfer RBU que podes confirmar.",
|
bankRulesEligibility5: "O PUB paga no seu seguinte ciclo e publica un transfer RBU que podes confirmar.",
|
||||||
bankRulesEligibility6: "Se falta o teu enderezo, o feed é demasiado novo, rexeitaches o mes ou o PUB non ten fondos, a reclamación sáltase e queda rexistrada no PUB.",
|
bankRulesEligibility6: "Se falta o teu enderezo, o feed é demasiado novo, rexeitaches o mes ou o PUB non ten fondos, a reclamación sáltase e queda rexistrada no PUB.",
|
||||||
copyLinkTitle: "Copiar a ligazón",
|
copyLinkTitle: "Copiar a ligazón",
|
||||||
|
cardMenuTitle: "Máis accións",
|
||||||
|
shareContent: "Compartir",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Rexeitar RBU",
|
bankRefuseUBI: "Rexeitar RBU",
|
||||||
|
|
@ -889,7 +886,7 @@ module.exports = {
|
||||||
walletNotConfigured: "A túa carteira de ECOin aínda non está configurada.",
|
walletNotConfigured: "A túa carteira de ECOin aínda non está configurada.",
|
||||||
walletSettingsDescription: "Integrar Oasis coa túa carteira de ECOin.",
|
walletSettingsDescription: "Integrar Oasis coa túa carteira de ECOin.",
|
||||||
walletSettingsMobileDescription: "Vincula Oasis coa app ECOin Wallet deste dispositivo.",
|
walletSettingsMobileDescription: "Vincula Oasis coa app ECOin Wallet deste dispositivo.",
|
||||||
walletDownloadEcoin: "Descargar ECOin!",
|
walletDownloadEcoin: "Descargar ECOin",
|
||||||
walletRpcUrl: "URL do RPC",
|
walletRpcUrl: "URL do RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Cantidade non válida",
|
invalid_amount: "Cantidade non válida",
|
||||||
|
|
@ -942,6 +939,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Actualizar",
|
bookmarkUpdateButton: "Actualizar",
|
||||||
bookmarkAddFavoriteButton: "Engadir a favoritos",
|
bookmarkAddFavoriteButton: "Engadir a favoritos",
|
||||||
bookmarkRemoveFavoriteButton: "Quitar de favoritos",
|
bookmarkRemoveFavoriteButton: "Quitar de favoritos",
|
||||||
|
bookmarkTitleLabel: "Título",
|
||||||
bookmarkUrlLabel: "Ligazón",
|
bookmarkUrlLabel: "Ligazón",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Descrición",
|
bookmarkDescriptionLabel: "Descrición",
|
||||||
|
|
@ -1086,9 +1084,9 @@ module.exports = {
|
||||||
allCVs: "CV",
|
allCVs: "CV",
|
||||||
discoverPeople: "Descobre habitantes na túa rede.",
|
discoverPeople: "Descobre habitantes na túa rede.",
|
||||||
searchInhabitantsPlaceholder: "Buscar en Habitantes...",
|
searchInhabitantsPlaceholder: "Buscar en Habitantes...",
|
||||||
filterLocation: "FILTRAR habitantes POR LOCALIZACIÓN …",
|
filterLocation: "Buscar por localización…",
|
||||||
filterLanguage: "FILTRAR habitantes POR IDIOMA …",
|
filterLanguage: "Buscar por idioma…",
|
||||||
filterSkills: "FILTRAR habitantes POR HABILIDADES …",
|
filterSkills: "Buscar por habilidades…",
|
||||||
cvSkillsCount: "Habilidades",
|
cvSkillsCount: "Habilidades",
|
||||||
applyFilters: "Aplicar os filtros",
|
applyFilters: "Aplicar os filtros",
|
||||||
locationLabel: "Localización",
|
locationLabel: "Localización",
|
||||||
|
|
@ -1104,6 +1102,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Máis actividade",
|
topactivitySectionTitle: "Máis actividade",
|
||||||
"TOP INACTIVITYButton": "MÁIS INACTIVOS",
|
"TOP INACTIVITYButton": "MÁIS INACTIVOS",
|
||||||
topinactivitySectionTitle: "Habitantes menos activos",
|
topinactivitySectionTitle: "Habitantes menos activos",
|
||||||
|
contactsSectionTitle: "Contactos",
|
||||||
blockedSectionTitle: "Bloqueados",
|
blockedSectionTitle: "Bloqueados",
|
||||||
gallerySectionTitle: "GALERÍA",
|
gallerySectionTitle: "GALERÍA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1558,9 +1557,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Data descoñecida",
|
voteUnknownDate: "Data descoñecida",
|
||||||
voteDetailSectionTitle: "Detalles da votación",
|
voteDetailSectionTitle: "Detalles da votación",
|
||||||
voteCommentsLabel: "Comentarios",
|
voteCommentsLabel: "Comentarios",
|
||||||
voteCommentsForumButton: "Abrir a conversa",
|
voteNoCommentsYet: "Aínda non hai comentarios.",
|
||||||
voteCommentsSectionTitle: "Conversa aberta",
|
|
||||||
voteNoCommentsYet: "Aínda non hai comentarios. Sé a primeira persoa en opinar.",
|
|
||||||
commentsGoToLast: "Ir ao último comentario",
|
commentsGoToLast: "Ir ao último comentario",
|
||||||
voteNewCommentPlaceholder: "Escribe aquí o teu comentario…",
|
voteNewCommentPlaceholder: "Escribe aquí o teu comentario…",
|
||||||
voteNewCommentButton: "Publicar o comentario",
|
voteNewCommentButton: "Publicar o comentario",
|
||||||
|
|
@ -1687,7 +1684,6 @@ module.exports = {
|
||||||
pollVoteButton: "Votar",
|
pollVoteButton: "Votar",
|
||||||
pollChangeVote: "Cambiar o voto",
|
pollChangeVote: "Cambiar o voto",
|
||||||
pollVoters: "Votantes",
|
pollVoters: "Votantes",
|
||||||
pollComments: "Comentarios",
|
|
||||||
pollStatusLabel: "Estado",
|
pollStatusLabel: "Estado",
|
||||||
pollStatusOpen: "ABERTA",
|
pollStatusOpen: "ABERTA",
|
||||||
pollStatusClosed: "PECHADA",
|
pollStatusClosed: "PECHADA",
|
||||||
|
|
@ -1709,9 +1705,22 @@ module.exports = {
|
||||||
blogAllowComments: "Permitir comentarios",
|
blogAllowComments: "Permitir comentarios",
|
||||||
blogCommentsClosed: "O autor pechou os comentarios deste blog.",
|
blogCommentsClosed: "O autor pechou os comentarios deste blog.",
|
||||||
blogNoItems: "Non se atoparon blogs.",
|
blogNoItems: "Non se atoparon blogs.",
|
||||||
viewBlog: "Ver o blog",
|
|
||||||
forumTitle: "Foros",
|
forumTitle: "Foros",
|
||||||
forumCategoryLabel: "Categoría",
|
forumCategoryLabel: "Categoría",
|
||||||
|
forumCatGENERAL: "XERAL",
|
||||||
|
forumCatPOLITICS: "POLÍTICA",
|
||||||
|
forumCatTECH: "TECNOLOXÍA",
|
||||||
|
forumCatSCIENCE: "CIENCIA",
|
||||||
|
forumCatMUSIC: "MÚSICA",
|
||||||
|
forumCatART: "ARTE",
|
||||||
|
forumCatGAMING: "XOGOS",
|
||||||
|
forumCatBOOKS: "LIBROS",
|
||||||
|
forumCatFILMS: "PELÍCULAS",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFÍA",
|
||||||
|
forumCatSOCIETY: "SOCIEDADE",
|
||||||
|
forumCatPRIVACY: "PRIVACIDADE",
|
||||||
|
forumCatCYBERWARFARE: "CIBERGUERRA",
|
||||||
|
forumCatSURVIVALISM: "SUPERVIVENCIA",
|
||||||
forumTitleLabel: "Título",
|
forumTitleLabel: "Título",
|
||||||
forumTitlePlaceholder: "Título do foro...",
|
forumTitlePlaceholder: "Título do foro...",
|
||||||
forumCreateButton: "Crear un foro",
|
forumCreateButton: "Crear un foro",
|
||||||
|
|
@ -1770,8 +1779,6 @@ module.exports = {
|
||||||
feedTitle: "Feed",
|
feedTitle: "Feed",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Abrir o debate",
|
feedOpenDiscussion: "Abrir o debate",
|
||||||
feedPostComment: "Publicar o comentario",
|
|
||||||
noComments: "Aínda non hai comentarios",
|
|
||||||
createFeedTitle: "Crear un feed",
|
createFeedTitle: "Crear un feed",
|
||||||
createFeedButton: "Enviar o feed!",
|
createFeedButton: "Enviar o feed!",
|
||||||
feedPlaceholder: "Que está a pasar? (máximo 280 caracteres)",
|
feedPlaceholder: "Que está a pasar? (máximo 280 caracteres)",
|
||||||
|
|
@ -1982,42 +1989,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Informes en revisión",
|
reportsUnderReviewSectionTitle: "Informes en revisión",
|
||||||
reportsResolvedSectionTitle: "Informes resoltos",
|
reportsResolvedSectionTitle: "Informes resoltos",
|
||||||
reportsInvalidSectionTitle: "Informes non válidos",
|
reportsInvalidSectionTitle: "Informes non válidos",
|
||||||
reportsBugTemplateTitle: "Datos para reproducir (erros)",
|
|
||||||
reportsFeatureTemplateTitle: "Detalles (funcións)",
|
|
||||||
reportsAbuseTemplateTitle: "Detalles (abuso)",
|
|
||||||
reportsContentTemplateTitle: "Detalles (problemas de contido)",
|
|
||||||
reportsStepsToReproduceLabel: "Pasos para reproducilo",
|
|
||||||
reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...",
|
|
||||||
reportsExpectedBehaviorLabel: "Resultado esperado",
|
|
||||||
reportsExpectedBehaviorPlaceholder: "Que debería pasar?",
|
|
||||||
reportsActualBehaviorLabel: "Resultado real",
|
|
||||||
reportsActualBehaviorPlaceholder: "Que pasa realmente?",
|
|
||||||
reportsEnvironmentLabel: "Contorno",
|
|
||||||
reportsEnvironmentPlaceholder: "Versión, dispositivo, SO, navegador, configuración, rexistros, etc.",
|
|
||||||
reportsReproduceRateLabel: "Frecuencia",
|
|
||||||
reportsReproduceRateAlways: "Sempre",
|
|
||||||
reportsReproduceRateOften: "A miúdo",
|
|
||||||
reportsReproduceRateSometimes: "Ás veces",
|
|
||||||
reportsReproduceRateRarely: "Raramente",
|
|
||||||
reportsReproduceRateUnable: "Non se pode reproducir",
|
|
||||||
reportsReproduceRateUnknown: "Non especificado",
|
|
||||||
reportsProblemStatementLabel: "Problema / necesidade",
|
|
||||||
reportsProblemStatementPlaceholder: "Que problema resolve esta función?",
|
|
||||||
reportsUserStoryLabel: "Historia de habitante",
|
|
||||||
reportsUserStoryPlaceholder: "Como <habitante>, quero <acción>, para <beneficio>.",
|
|
||||||
reportsAcceptanceCriteriaLabel: "Criterios de aceptación",
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: "- Dado...\n- Cando...\n- Entón...",
|
|
||||||
reportsWhatHappenedLabel: "Que ocorreu?",
|
|
||||||
reportsWhatHappenedPlaceholder: "Describe o incidente con contexto e datas aproximadas.",
|
|
||||||
reportsReportedUserLabel: "Habitante / entidade denunciada",
|
|
||||||
reportsReportedUserPlaceholder: "@habitante, feed, ID, ligazón, etc.",
|
|
||||||
reportsEvidenceLinksLabel: "Probas / ligazóns",
|
|
||||||
reportsEvidenceLinksPlaceholder: "Pega ligazóns, IDs de mensaxes, capturas (se procede), etc.",
|
|
||||||
reportsContentLocationLabel: "Onde está o contido",
|
|
||||||
reportsContentLocationPlaceholder: "Ligazón, ID, canle, fío, autor, etc.",
|
|
||||||
reportsWhyInappropriatePlaceholder: "Explica o motivo e o impacto.",
|
|
||||||
reportsRequestedActionLabel: "Acción solicitada",
|
|
||||||
reportsRequestedActionPlaceholder: "Eliminar, agochar, marcar, advertir, etc.",
|
|
||||||
tribesTitle: "Tribos",
|
tribesTitle: "Tribos",
|
||||||
tribeFilterAll: "TODAS",
|
tribeFilterAll: "TODAS",
|
||||||
tribeFilterRecent: "RECENTES",
|
tribeFilterRecent: "RECENTES",
|
||||||
|
|
@ -2261,6 +2232,7 @@ module.exports = {
|
||||||
voteWellResearched: "Ben investigado",
|
voteWellResearched: "Ben investigado",
|
||||||
voteNeedsSources: "Necesita fontes",
|
voteNeedsSources: "Necesita fontes",
|
||||||
voteWrong: "Incorrecto",
|
voteWrong: "Incorrecto",
|
||||||
|
voteOutdated: "Desactualizado",
|
||||||
voteLowQuality: "Baixa calidade",
|
voteLowQuality: "Baixa calidade",
|
||||||
voteLove: "Amor",
|
voteLove: "Amor",
|
||||||
voteClear: "Claro",
|
voteClear: "Claro",
|
||||||
|
|
@ -2505,7 +2477,6 @@ module.exports = {
|
||||||
shopNoProducts: "Aínda non hai produtos.",
|
shopNoProducts: "Aínda non hai produtos.",
|
||||||
shopOutOfStock: "Esgotado",
|
shopOutOfStock: "Esgotado",
|
||||||
shopBuy: "Comprar",
|
shopBuy: "Comprar",
|
||||||
shopBuyEncryptedNote: "Os teus datos de entrega envíanse cifrados; só o dono da tenda pode velos.",
|
|
||||||
shopBuyDeliveryAddress: "Enderezo de entrega",
|
shopBuyDeliveryAddress: "Enderezo de entrega",
|
||||||
shopBuyContact: "Contacto",
|
shopBuyContact: "Contacto",
|
||||||
shopBuyContactPlaceholder: "correo, teléfono, etc.",
|
shopBuyContactPlaceholder: "correo, teléfono, etc.",
|
||||||
|
|
@ -3022,6 +2993,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Descrición",
|
industryDescriptionLabel: "Descrición",
|
||||||
industryDescriptionPlaceholder: "Que produce esta fábrica?",
|
industryDescriptionPlaceholder: "Que produce esta fábrica?",
|
||||||
industrySector: "Sector",
|
industrySector: "Sector",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Agricultura",
|
||||||
|
industrySector_textile: "Téxtil",
|
||||||
|
industrySector_energy: "Enerxía",
|
||||||
|
industrySector_food: "Alimentación",
|
||||||
|
industrySector_construction: "Construción",
|
||||||
|
industrySector_media: "Medios",
|
||||||
|
industrySector_services: "Servizos",
|
||||||
|
industrySector_other: "Outros",
|
||||||
industryMembershipPolicy: "Política de pertenza",
|
industryMembershipPolicy: "Política de pertenza",
|
||||||
industryQuorum: "Quórum",
|
industryQuorum: "Quórum",
|
||||||
industryMajority: "Maioría",
|
industryMajority: "Maioría",
|
||||||
|
|
@ -3223,6 +3204,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Módulo para xestionar o teu contido favorito.",
|
modulesFavoritesDescription: "Módulo para xestionar o teu contido favorito.",
|
||||||
fileTooLargeTitle: "Ficheiro demasiado grande",
|
fileTooLargeTitle: "Ficheiro demasiado grande",
|
||||||
fileTooLargeMessage: "O ficheiro supera o tamaño máximo permitido (75 MB). Por favor, selecciona un ficheiro máis pequeno.",
|
fileTooLargeMessage: "O ficheiro supera o tamaño máximo permitido (75 MB). Por favor, selecciona un ficheiro máis pequeno.",
|
||||||
|
fileWrongType: "Este ficheiro non é do tipo axeitado para esta sección.",
|
||||||
|
mediaLockedAfterOpinions: "Xa ten opinións: podes editar o resto, pero non cambiar o que se valorou.",
|
||||||
|
pdfPrevPage: "Páxina anterior",
|
||||||
|
pdfNextPage: "Páxina seguinte",
|
||||||
|
pdfPage: "Páxina",
|
||||||
|
pdfZoomIn: "Achegar",
|
||||||
|
pdfZoomOut: "Afastar",
|
||||||
|
pdfFullscreen: "Pantalla completa",
|
||||||
|
pdfClose: "Pechar",
|
||||||
|
pdfRotate: "Xirar",
|
||||||
|
contentNotAvailableYet: "Este contido aínda non está dispoñible.",
|
||||||
|
pdfRetry: "Tentar de novo",
|
||||||
goBack: "Volver",
|
goBack: "Volver",
|
||||||
errorDismiss: "De acordo",
|
errorDismiss: "De acordo",
|
||||||
errorPageTitle: "Produciuse un erro",
|
errorPageTitle: "Produciuse un erro",
|
||||||
|
|
@ -3267,7 +3260,6 @@ module.exports = {
|
||||||
invitesFollow: "Seguir",
|
invitesFollow: "Seguir",
|
||||||
invitesUnfollowedInvites: "Non federadas",
|
invitesUnfollowedInvites: "Non federadas",
|
||||||
invitesNoUnfollowed: "Non hai pubs non federados.",
|
invitesNoUnfollowed: "Non hai pubs non federados.",
|
||||||
reportsWhyInappropriateLabel: "Por que é inadecuado?",
|
|
||||||
blockchainContentDeleted: "Este contido foi eliminado",
|
blockchainContentDeleted: "Este contido foi eliminado",
|
||||||
visitContent: "Visitar o contido",
|
visitContent: "Visitar o contido",
|
||||||
favoriteAdd: "Fixar en favoritos",
|
favoriteAdd: "Fixar en favoritos",
|
||||||
|
|
@ -3579,6 +3571,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
podcastTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
||||||
podcastFilterAll: "TODOS",
|
podcastFilterAll: "TODOS",
|
||||||
podcastFilterMine: "MEUS",
|
podcastFilterMine: "MEUS",
|
||||||
|
podcastFilterFavorites: "FAVORITOS",
|
||||||
|
podcastEpisodeNumberLabel: "Número de episodio",
|
||||||
podcastFilterRecent: "RECENTES",
|
podcastFilterRecent: "RECENTES",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "OÍNTES",
|
podcastFilterViewers: "OÍNTES",
|
||||||
|
|
@ -3797,7 +3791,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Volver á páxina",
|
wikiBackToPage: "Volver á páxina",
|
||||||
wikiLinkedChip: "Ligada",
|
wikiLinkedChip: "Ligada",
|
||||||
wikiRestoredSummary: "Versión restaurada",
|
wikiRestoredSummary: "Versión restaurada",
|
||||||
wikiExportPdf: "Xerar a páxina",
|
|
||||||
wikiPreview: "Vista previa",
|
wikiPreview: "Vista previa",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Crear unha wiki",
|
tribeWikiCreate: "Crear unha wiki",
|
||||||
|
|
@ -3865,6 +3858,9 @@ module.exports = {
|
||||||
roomRecStop: "Parar a gravación",
|
roomRecStop: "Parar a gravación",
|
||||||
roomStreamFile: "Emitir",
|
roomStreamFile: "Emitir",
|
||||||
roomStreamStop: "Parar",
|
roomStreamStop: "Parar",
|
||||||
|
roomScreen: "Pantalla",
|
||||||
|
roomScreenSharing: "Compartindo pantalla",
|
||||||
|
roomScreenWarning: "Todos na sala verán a túa pantalla completa ata que pares.",
|
||||||
roomStreaming: "Emitindo",
|
roomStreaming: "Emitindo",
|
||||||
liveFullscreen: "Pantalla completa",
|
liveFullscreen: "Pantalla completa",
|
||||||
roomStreamPickTitle: "Escolle un vídeo",
|
roomStreamPickTitle: "Escolle un vídeo",
|
||||||
|
|
@ -3922,6 +3918,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Xa formas parte. Podes abrilo desde o seu módulo.",
|
inviteAlreadyMember: "Xa formas parte. Podes abrilo desde o seu módulo.",
|
||||||
actionFailed: "Non se puido facer. Téntao de novo.",
|
actionFailed: "Non se puido facer. Téntao de novo.",
|
||||||
publishTooShort: "A túa publicación é demasiado curta.",
|
publishTooShort: "A túa publicación é demasiado curta.",
|
||||||
|
publishEmpty: "Escribe algo ou achega un ficheiro.",
|
||||||
walletSendFailed: "Non se puido enviar o pagamento: {reason}",
|
walletSendFailed: "Non se puido enviar o pagamento: {reason}",
|
||||||
pixeliaCoordsError: "Esas coordenadas están fóra da grella.",
|
pixeliaCoordsError: "Esas coordenadas están fóra da grella.",
|
||||||
courtsErrTitle: "Cómpre un título.",
|
courtsErrTitle: "Cómpre un título.",
|
||||||
|
|
@ -4434,6 +4431,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Etiquetas",
|
torrentTagsLabel: "Etiquetas",
|
||||||
torrentTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
torrentTagsPlaceholder: "Introduce etiquetas separadas por comas",
|
||||||
torrentSizeLabel: "Tamaño",
|
torrentSizeLabel: "Tamaño",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Ficheiros",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Xerar torrent",
|
torrentFromContent: "Xerar torrent",
|
||||||
torrentFromContentMissing: "O ficheiro aínda non está dispoñible neste nodo. Téntao máis tarde.",
|
torrentFromContentMissing: "O ficheiro aínda non está dispoñible neste nodo. Téntao máis tarde.",
|
||||||
torrentDownloads: "Descargas",
|
torrentDownloads: "Descargas",
|
||||||
|
|
@ -4519,9 +4519,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Aceptar",
|
inhabitantsPendingAccept: "Aceptar",
|
||||||
inhabitantsPendingReject: "Rexeitar",
|
inhabitantsPendingReject: "Rexeitar",
|
||||||
tribeSectionGovernance: "GOBERNO",
|
tribeSectionGovernance: "GOBERNO",
|
||||||
tribePadInviteRequired: "Non tes acceso ao pad. Pide un convite para acceder ao contido.",
|
|
||||||
tribeChatInviteRequired: "Non tes acceso ao chat. Pide un convite para acceder ao contido.",
|
|
||||||
tribeGovernanceDesc: "Goberno interno desta tribo.",
|
|
||||||
tribeGovernanceNoGov: "Sen goberno activo",
|
tribeGovernanceNoGov: "Sen goberno activo",
|
||||||
tribeGovernanceNoGovDesc: "Esta tribo aínda non ten goberno. Propón candidaturas para iniciar o proceso.",
|
tribeGovernanceNoGovDesc: "Esta tribo aínda non ten goberno. Propón candidaturas para iniciar o proceso.",
|
||||||
tribeGovCardTitle: "Goberno actual",
|
tribeGovCardTitle: "Goberno actual",
|
||||||
|
|
@ -4578,7 +4575,6 @@ module.exports = {
|
||||||
logsFilterMonth: "ÚLTIMO MES",
|
logsFilterMonth: "ÚLTIMO MES",
|
||||||
logsFilterWeek: "ÚLTIMA SEMANA",
|
logsFilterWeek: "ÚLTIMA SEMANA",
|
||||||
logsFilterToday: "HOXE",
|
logsFilterToday: "HOXE",
|
||||||
logsComments: "Comentarios",
|
|
||||||
logsAuthorEntries: "Entradas",
|
logsAuthorEntries: "Entradas",
|
||||||
logsCreate: "Crear unha entrada",
|
logsCreate: "Crear unha entrada",
|
||||||
logsExport: "Exportar a bitácora",
|
logsExport: "Exportar a bitácora",
|
||||||
|
|
@ -4657,6 +4653,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periodicamente este nodo a outras instancias de Oasis na mesma rede local.",
|
settingsLanDesc: "Anuncia periodicamente este nodo a outras instancias de Oasis na mesma rede local.",
|
||||||
settingsLanEnable: "Activar a difusión LAN",
|
settingsLanEnable: "Activar a difusión LAN",
|
||||||
|
settingsConnectionsTitle: "Conexións",
|
||||||
|
settingsConnectionsDesc: "Cantos peers mantén conectados este nodo á vez.",
|
||||||
settingsLanTitle: "Difusión LAN",
|
settingsLanTitle: "Difusión LAN",
|
||||||
settingsReplicationTitle: "Replicación",
|
settingsReplicationTitle: "Replicación",
|
||||||
settingsReplicationDesc: "Configura o número de saltos que o teu nodo segue desde o teu propio feed ao replicar contido.",
|
settingsReplicationDesc: "Configura o número de saltos que o teu nodo segue desde o teu propio feed ao replicar contido.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← मेरा नेटवर्क",
|
graphosBackToMine: "← मेरा नेटवर्क",
|
||||||
graphosShowing: "दिखा रहे",
|
graphosShowing: "दिखा रहे",
|
||||||
graphosYou: "आप",
|
graphosYou: "आप",
|
||||||
graphosTotalNodes: "कुल नोड्स",
|
graphosTotalNodes: "कुल",
|
||||||
|
onlinePlural: "ऑनलाइन",
|
||||||
mentions: "उल्लेख",
|
mentions: "उल्लेख",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("हर संदेश जो आपको @उल्लेख करता है"),
|
strong("हर संदेश जो आपको @उल्लेख करता है"),
|
||||||
|
|
@ -420,6 +421,7 @@ module.exports = {
|
||||||
menuMedia: "पुस्तकालय",
|
menuMedia: "पुस्तकालय",
|
||||||
menuTools: "उपकरण",
|
menuTools: "उपकरण",
|
||||||
comment: "टिप्पणी",
|
comment: "टिप्पणी",
|
||||||
|
typeComment: "टिप्पणी",
|
||||||
subtopic: "उपविषय",
|
subtopic: "उपविषय",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "समर्थन वापस लें",
|
unfollow: "समर्थन वापस लें",
|
||||||
|
|
@ -449,9 +451,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।",
|
"याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -730,10 +729,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"प्रश्न",
|
votesQuestionLabel:"प्रश्न",
|
||||||
voteStatusOpen:"खुला",
|
voteStatusOpen:"खुला",
|
||||||
voteStatusClosed:"बंद",
|
voteStatusClosed:"बंद",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" ने टिप्पणी की ",
|
|
||||||
a({ href: parentUrl }, " थ्रेड पर"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`@${authorName} की पोस्ट पर टिप्पणी`],
|
commentTitle: ({ authorName }) => [`@${authorName} की पोस्ट पर टिप्पणी`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" ने एक उपविषय बनाया ",
|
" ने एक उपविषय बनाया ",
|
||||||
|
|
@ -868,6 +863,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "PUB अगले चक्र में भुगतान करता है और एक UBI ट्रांसफर प्रकाशित करता है जिसे आप पुष्टि कर सकते हैं।",
|
bankRulesEligibility5: "PUB अगले चक्र में भुगतान करता है और एक UBI ट्रांसफर प्रकाशित करता है जिसे आप पुष्टि कर सकते हैं।",
|
||||||
bankRulesEligibility6: "यदि पता नहीं है, फ़ीड बहुत नई है, आपने माह अस्वीकार किया या PUB के पास धन नहीं है, तो दावा छोड़ दिया जाता है और PUB पर लॉग किया जाता है।",
|
bankRulesEligibility6: "यदि पता नहीं है, फ़ीड बहुत नई है, आपने माह अस्वीकार किया या PUB के पास धन नहीं है, तो दावा छोड़ दिया जाता है और PUB पर लॉग किया जाता है।",
|
||||||
copyLinkTitle: "लिंक कॉपी करें",
|
copyLinkTitle: "लिंक कॉपी करें",
|
||||||
|
cardMenuTitle: "और कार्रवाइयाँ",
|
||||||
|
shareContent: "साझा करें",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "UBI अस्वीकार करें",
|
bankRefuseUBI: "UBI अस्वीकार करें",
|
||||||
|
|
@ -901,7 +898,7 @@ module.exports = {
|
||||||
walletNotConfigured: "आपका ECOin वॉलेट अभी कॉन्फ़िगर नहीं है।",
|
walletNotConfigured: "आपका ECOin वॉलेट अभी कॉन्फ़िगर नहीं है।",
|
||||||
walletSettingsDescription: "Oasis को अपने ECOin वॉलेट के साथ एकीकृत करें।",
|
walletSettingsDescription: "Oasis को अपने ECOin वॉलेट के साथ एकीकृत करें।",
|
||||||
walletSettingsMobileDescription: "Oasis को इस डिवाइस के ECOin Wallet ऐप से जोड़ें।",
|
walletSettingsMobileDescription: "Oasis को इस डिवाइस के ECOin Wallet ऐप से जोड़ें।",
|
||||||
walletDownloadEcoin: "ECOin डाउनलोड करें!",
|
walletDownloadEcoin: "ECOin डाउनलोड करें",
|
||||||
walletRpcUrl: "RPC URL",
|
walletRpcUrl: "RPC URL",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "अमान्य राशि",
|
invalid_amount: "अमान्य राशि",
|
||||||
|
|
@ -954,6 +951,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "अपडेट",
|
bookmarkUpdateButton: "अपडेट",
|
||||||
bookmarkAddFavoriteButton: "पसंदीदा जोड़ें",
|
bookmarkAddFavoriteButton: "पसंदीदा जोड़ें",
|
||||||
bookmarkRemoveFavoriteButton: "पसंदीदा हटाएँ",
|
bookmarkRemoveFavoriteButton: "पसंदीदा हटाएँ",
|
||||||
|
bookmarkTitleLabel: "शीर्षक",
|
||||||
bookmarkUrlLabel: "लिंक",
|
bookmarkUrlLabel: "लिंक",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "विवरण",
|
bookmarkDescriptionLabel: "विवरण",
|
||||||
|
|
@ -1098,9 +1096,9 @@ module.exports = {
|
||||||
allCVs: "CVs",
|
allCVs: "CVs",
|
||||||
discoverPeople: "अपने नेटवर्क में निवासियों को खोजें।",
|
discoverPeople: "अपने नेटवर्क में निवासियों को खोजें।",
|
||||||
searchInhabitantsPlaceholder: "निवासी में खोजें...",
|
searchInhabitantsPlaceholder: "निवासी में खोजें...",
|
||||||
filterLocation: "स्थान से निवासी फ़िल्टर करें …",
|
filterLocation: "स्थान से खोजें…",
|
||||||
filterLanguage: "भाषा से निवासी फ़िल्टर करें …",
|
filterLanguage: "भाषा से खोजें…",
|
||||||
filterSkills: "कौशल से निवासी फ़िल्टर करें …",
|
filterSkills: "कौशल से खोजें…",
|
||||||
cvSkillsCount: "कौशल",
|
cvSkillsCount: "कौशल",
|
||||||
applyFilters: "फ़िल्टर लागू करें",
|
applyFilters: "फ़िल्टर लागू करें",
|
||||||
locationLabel: "स्थान",
|
locationLabel: "स्थान",
|
||||||
|
|
@ -1116,6 +1114,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "शीर्ष गतिविधि",
|
topactivitySectionTitle: "शीर्ष गतिविधि",
|
||||||
"TOP INACTIVITYButton": "सबसे निष्क्रिय",
|
"TOP INACTIVITYButton": "सबसे निष्क्रिय",
|
||||||
topinactivitySectionTitle: "सबसे कम सक्रिय निवासी",
|
topinactivitySectionTitle: "सबसे कम सक्रिय निवासी",
|
||||||
|
contactsSectionTitle: "संपर्क",
|
||||||
blockedSectionTitle: "ब्लॉक किए गए",
|
blockedSectionTitle: "ब्लॉक किए गए",
|
||||||
gallerySectionTitle: "गैलरी",
|
gallerySectionTitle: "गैलरी",
|
||||||
topSectionTitle: "शीर्ष",
|
topSectionTitle: "शीर्ष",
|
||||||
|
|
@ -1569,9 +1568,7 @@ module.exports = {
|
||||||
voteUnknownDate: "अज्ञात दिनांक",
|
voteUnknownDate: "अज्ञात दिनांक",
|
||||||
voteDetailSectionTitle: 'मत विवरण',
|
voteDetailSectionTitle: 'मत विवरण',
|
||||||
voteCommentsLabel: 'टिप्पणियाँ',
|
voteCommentsLabel: 'टिप्पणियाँ',
|
||||||
voteCommentsForumButton: 'चर्चा खोलें',
|
voteNoCommentsYet: 'अभी तक कोई टिप्पणी नहीं है।',
|
||||||
voteCommentsSectionTitle: 'चर्चा खोलें',
|
|
||||||
voteNoCommentsYet: 'अभी तक कोई टिप्पणी नहीं है। पहले उत्तर देने वाले बनें।',
|
|
||||||
commentsGoToLast: "अंतिम टिप्पणी पर जाएँ",
|
commentsGoToLast: "अंतिम टिप्पणी पर जाएँ",
|
||||||
voteNewCommentPlaceholder: 'अपनी टिप्पणी यहाँ लिखें…',
|
voteNewCommentPlaceholder: 'अपनी टिप्पणी यहाँ लिखें…',
|
||||||
voteNewCommentButton: 'टिप्पणी पोस्ट करें',
|
voteNewCommentButton: 'टिप्पणी पोस्ट करें',
|
||||||
|
|
@ -1698,7 +1695,6 @@ module.exports = {
|
||||||
pollVoteButton: "मत दें",
|
pollVoteButton: "मत दें",
|
||||||
pollChangeVote: "वोट बदलें",
|
pollChangeVote: "वोट बदलें",
|
||||||
pollVoters: "मतदाता",
|
pollVoters: "मतदाता",
|
||||||
pollComments: "टिप्पणियाँ",
|
|
||||||
pollStatusLabel: "स्थिति",
|
pollStatusLabel: "स्थिति",
|
||||||
pollStatusOpen: "खुला",
|
pollStatusOpen: "खुला",
|
||||||
pollStatusClosed: "बंद",
|
pollStatusClosed: "बंद",
|
||||||
|
|
@ -1720,9 +1716,22 @@ module.exports = {
|
||||||
blogAllowComments: "टिप्पणियाँ अनुमति दें",
|
blogAllowComments: "टिप्पणियाँ अनुमति दें",
|
||||||
blogCommentsClosed: "लेखक ने इस ब्लॉग की टिप्पणियाँ बंद कर दी हैं।",
|
blogCommentsClosed: "लेखक ने इस ब्लॉग की टिप्पणियाँ बंद कर दी हैं।",
|
||||||
blogNoItems: "कोई ब्लॉग नहीं मिला।",
|
blogNoItems: "कोई ब्लॉग नहीं मिला।",
|
||||||
viewBlog: "ब्लॉग देखें",
|
|
||||||
forumTitle: "मंच",
|
forumTitle: "मंच",
|
||||||
forumCategoryLabel: "श्रेणी",
|
forumCategoryLabel: "श्रेणी",
|
||||||
|
forumCatGENERAL: "सामान्य",
|
||||||
|
forumCatPOLITICS: "राजनीति",
|
||||||
|
forumCatTECH: "तकनीक",
|
||||||
|
forumCatSCIENCE: "विज्ञान",
|
||||||
|
forumCatMUSIC: "संगीत",
|
||||||
|
forumCatART: "कला",
|
||||||
|
forumCatGAMING: "गेमिंग",
|
||||||
|
forumCatBOOKS: "किताबें",
|
||||||
|
forumCatFILMS: "फ़िल्में",
|
||||||
|
forumCatPHILOSOPHY: "दर्शन",
|
||||||
|
forumCatSOCIETY: "समाज",
|
||||||
|
forumCatPRIVACY: "निजता",
|
||||||
|
forumCatCYBERWARFARE: "साइबर युद्ध",
|
||||||
|
forumCatSURVIVALISM: "उत्तरजीविता",
|
||||||
forumTitleLabel: "शीर्षक",
|
forumTitleLabel: "शीर्षक",
|
||||||
forumTitlePlaceholder: "मंच शीर्षक...",
|
forumTitlePlaceholder: "मंच शीर्षक...",
|
||||||
forumCreateButton: "मंच बनाएँ",
|
forumCreateButton: "मंच बनाएँ",
|
||||||
|
|
@ -1991,43 +2000,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "समीक्षाधीन रिपोर्ट",
|
reportsUnderReviewSectionTitle: "समीक्षाधीन रिपोर्ट",
|
||||||
reportsResolvedSectionTitle: "हल की गई रिपोर्ट",
|
reportsResolvedSectionTitle: "हल की गई रिपोर्ट",
|
||||||
reportsInvalidSectionTitle: "अमान्य रिपोर्ट",
|
reportsInvalidSectionTitle: "अमान्य रिपोर्ट",
|
||||||
reportsBugTemplateTitle: 'पुनरुत्पादन विवरण (बग)',
|
|
||||||
reportsFeatureTemplateTitle: 'विवरण (सुविधाएँ)',
|
|
||||||
reportsAbuseTemplateTitle: 'विवरण (दुरुपयोग)',
|
|
||||||
reportsContentTemplateTitle: 'विवरण (सामग्री समस्याएँ)',
|
|
||||||
reportsStepsToReproduceLabel: 'पुनरुत्पादन के चरण',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'अपेक्षित परिणाम',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'क्या होना चाहिए?',
|
|
||||||
reportsActualBehaviorLabel: 'वास्तविक परिणाम',
|
|
||||||
reportsActualBehaviorPlaceholder: 'वास्तव में क्या होता है?',
|
|
||||||
reportsEnvironmentLabel: 'वातावरण',
|
|
||||||
reportsEnvironmentPlaceholder: 'संस्करण, उपकरण, OS, ब्राउज़र, सेटिंग्स, लॉग, आदि।',
|
|
||||||
reportsReproduceRateLabel: 'पुनरुत्पादन दर',
|
|
||||||
reportsReproduceRateAlways: 'हमेशा',
|
|
||||||
reportsReproduceRateOften: 'अक्सर',
|
|
||||||
reportsReproduceRateSometimes: 'कभी-कभी',
|
|
||||||
reportsReproduceRateRarely: 'शायद ही कभी',
|
|
||||||
reportsReproduceRateUnable: 'पुनरुत्पादन करने में असमर्थ',
|
|
||||||
reportsReproduceRateUnknown: 'निर्दिष्ट नहीं',
|
|
||||||
reportsProblemStatementLabel: 'समस्या / आवश्यकता',
|
|
||||||
reportsProblemStatementPlaceholder: 'यह सुविधा कौन सी समस्या हल करती है?',
|
|
||||||
reportsUserStoryLabel: 'निवासी कहानी',
|
|
||||||
reportsUserStoryPlaceholder: 'एक <निवासी> के रूप में, मैं <कार्रवाई> चाहता हूँ, ताकि <लाभ>।',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'स्वीकृति मानदंड',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- दिया गया...\n- जब...\n- तब...',
|
|
||||||
reportsWhatHappenedLabel: 'क्या हुआ?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'संदर्भ और अनुमानित तिथियों के साथ घटना का वर्णन करें।',
|
|
||||||
reportsReportedUserLabel: 'रिपोर्ट किया गया निवासी / संस्था',
|
|
||||||
reportsReportedUserPlaceholder: '@निवासी, फ़ीड, ID, लिंक, आदि।',
|
|
||||||
reportsEvidenceLinksLabel: 'साक्ष्य / लिंक',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'लिंक, संदेश IDs, स्क्रीनशॉट (यदि लागू हो), आदि पेस्ट करें।',
|
|
||||||
reportsContentLocationLabel: 'सामग्री कहाँ है',
|
|
||||||
reportsContentLocationPlaceholder: 'लिंक, ID, चैनल, थ्रेड, लेखक, आदि।',
|
|
||||||
reportsWhyInappropriateLabel: "यह अनुचित क्यों है",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'कारण और प्रभाव बताएँ।',
|
|
||||||
reportsRequestedActionLabel: 'अनुरोधित कार्रवाई',
|
|
||||||
reportsRequestedActionPlaceholder: 'हटाएँ, छुपाएँ, टैग करें, चेतावनी दें, आदि।',
|
|
||||||
tribesTitle: "जनजातियाँ",
|
tribesTitle: "जनजातियाँ",
|
||||||
tribeFilterAll: "सभी",
|
tribeFilterAll: "सभी",
|
||||||
tribeFilterRecent: "हाल के",
|
tribeFilterRecent: "हाल के",
|
||||||
|
|
@ -2271,6 +2243,7 @@ module.exports = {
|
||||||
voteWellResearched: "अच्छी तरह शोधित",
|
voteWellResearched: "अच्छी तरह शोधित",
|
||||||
voteNeedsSources: "स्रोत चाहिए",
|
voteNeedsSources: "स्रोत चाहिए",
|
||||||
voteWrong: "गलत",
|
voteWrong: "गलत",
|
||||||
|
voteOutdated: "पुराना",
|
||||||
voteLowQuality: "निम्न गुणवत्ता",
|
voteLowQuality: "निम्न गुणवत्ता",
|
||||||
voteLove: "प्यार",
|
voteLove: "प्यार",
|
||||||
voteClear: "स्पष्ट",
|
voteClear: "स्पष्ट",
|
||||||
|
|
@ -2515,7 +2488,6 @@ module.exports = {
|
||||||
shopNoProducts: "अभी तक कोई उत्पाद नहीं।",
|
shopNoProducts: "अभी तक कोई उत्पाद नहीं।",
|
||||||
shopOutOfStock: "स्टॉक में नहीं",
|
shopOutOfStock: "स्टॉक में नहीं",
|
||||||
shopBuy: "खरीदें",
|
shopBuy: "खरीदें",
|
||||||
shopBuyEncryptedNote: "आपके डिलीवरी विवरण एन्क्रिप्टेड भेजे जाते हैं और केवल दुकान मालिक देख सकता है।",
|
|
||||||
shopBuyDeliveryAddress: "डिलीवरी पता",
|
shopBuyDeliveryAddress: "डिलीवरी पता",
|
||||||
shopBuyContact: "संपर्क",
|
shopBuyContact: "संपर्क",
|
||||||
shopBuyContactPlaceholder: "ईमेल, फोन, आदि।",
|
shopBuyContactPlaceholder: "ईमेल, फोन, आदि।",
|
||||||
|
|
@ -3032,6 +3004,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "विवरण",
|
industryDescriptionLabel: "विवरण",
|
||||||
industryDescriptionPlaceholder: "यह फैक्टरी क्या उत्पादित करती है?",
|
industryDescriptionPlaceholder: "यह फैक्टरी क्या उत्पादित करती है?",
|
||||||
industrySector: "क्षेत्र",
|
industrySector: "क्षेत्र",
|
||||||
|
industrySector_software: "सॉफ़्टवेयर",
|
||||||
|
industrySector_hardware: "हार्डवेयर",
|
||||||
|
industrySector_agriculture: "कृषि",
|
||||||
|
industrySector_textile: "वस्त्र",
|
||||||
|
industrySector_energy: "ऊर्जा",
|
||||||
|
industrySector_food: "खाद्य",
|
||||||
|
industrySector_construction: "निर्माण",
|
||||||
|
industrySector_media: "मीडिया",
|
||||||
|
industrySector_services: "सेवाएँ",
|
||||||
|
industrySector_other: "अन्य",
|
||||||
industryMembershipPolicy: "सदस्यता नीति",
|
industryMembershipPolicy: "सदस्यता नीति",
|
||||||
industryQuorum: "कोरम",
|
industryQuorum: "कोरम",
|
||||||
industryMajority: "बहुमत",
|
industryMajority: "बहुमत",
|
||||||
|
|
@ -3233,6 +3215,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "अपनी पसंदीदा सामग्री प्रबंधित करने का मॉड्यूल।",
|
modulesFavoritesDescription: "अपनी पसंदीदा सामग्री प्रबंधित करने का मॉड्यूल।",
|
||||||
fileTooLargeTitle: "फ़ाइल बहुत बड़ी",
|
fileTooLargeTitle: "फ़ाइल बहुत बड़ी",
|
||||||
fileTooLargeMessage: "फ़ाइल अधिकतम अनुमत आकार (75 MB) से अधिक है। कृपया एक छोटी फ़ाइल चुनें।",
|
fileTooLargeMessage: "फ़ाइल अधिकतम अनुमत आकार (75 MB) से अधिक है। कृपया एक छोटी फ़ाइल चुनें।",
|
||||||
|
fileWrongType: "यह फ़ाइल इस अनुभाग के लिए सही प्रकार की नहीं है।",
|
||||||
|
mediaLockedAfterOpinions: "इस पर पहले से राय हैं: आप बाकी संपादित कर सकते हैं, लेकिन जिसका मूल्यांकन हुआ उसे नहीं बदल सकते।",
|
||||||
|
pdfPrevPage: "पिछला पृष्ठ",
|
||||||
|
pdfNextPage: "अगला पृष्ठ",
|
||||||
|
pdfPage: "पृष्ठ",
|
||||||
|
pdfZoomIn: "ज़ूम इन",
|
||||||
|
pdfZoomOut: "ज़ूम आउट",
|
||||||
|
pdfFullscreen: "पूर्ण स्क्रीन",
|
||||||
|
pdfClose: "बंद करें",
|
||||||
|
pdfRotate: "घुमाएँ",
|
||||||
|
contentNotAvailableYet: "यह सामग्री अभी उपलब्ध नहीं है।",
|
||||||
|
pdfRetry: "फिर से कोशिश करें",
|
||||||
goBack: "वापस जाएँ",
|
goBack: "वापस जाएँ",
|
||||||
errorDismiss: "ठीक",
|
errorDismiss: "ठीक",
|
||||||
errorPageTitle: "कुछ गलत हो गया",
|
errorPageTitle: "कुछ गलत हो गया",
|
||||||
|
|
@ -3275,9 +3269,7 @@ module.exports = {
|
||||||
uploadMedia: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)",
|
uploadMedia: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)",
|
||||||
feedOpenDiscussion: "चर्चा खोलें",
|
feedOpenDiscussion: "चर्चा खोलें",
|
||||||
feedDetailTitle: "फ़ीड",
|
feedDetailTitle: "फ़ीड",
|
||||||
feedPostComment: "टिप्पणी पोस्ट करें",
|
|
||||||
courtsRespondentInvalid: "एक वैध SSB ID होनी चाहिए (@...ed25519)",
|
courtsRespondentInvalid: "एक वैध SSB ID होनी चाहिए (@...ed25519)",
|
||||||
noComments: "अभी तक कोई टिप्पणी नहीं",
|
|
||||||
mapsLabel: "मानचित्र",
|
mapsLabel: "मानचित्र",
|
||||||
mapTitle: "मानचित्र",
|
mapTitle: "मानचित्र",
|
||||||
mapDescription: "अपने नेटवर्क में ऑफ़लाइन मानचित्र प्रबंधित और साझा करें।",
|
mapDescription: "अपने नेटवर्क में ऑफ़लाइन मानचित्र प्रबंधित और साझा करें।",
|
||||||
|
|
@ -3581,6 +3573,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें",
|
podcastTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें",
|
||||||
podcastFilterAll: "सभी",
|
podcastFilterAll: "सभी",
|
||||||
podcastFilterMine: "मेरे",
|
podcastFilterMine: "मेरे",
|
||||||
|
podcastFilterFavorites: "पसंदीदा",
|
||||||
|
podcastEpisodeNumberLabel: "एपिसोड संख्या",
|
||||||
podcastFilterRecent: "हाल के",
|
podcastFilterRecent: "हाल के",
|
||||||
podcastFilterTop: "टॉप",
|
podcastFilterTop: "टॉप",
|
||||||
podcastFilterViewers: "श्रोता",
|
podcastFilterViewers: "श्रोता",
|
||||||
|
|
@ -3799,7 +3793,6 @@ module.exports = {
|
||||||
wikiBackToPage: "पृष्ठ पर वापस",
|
wikiBackToPage: "पृष्ठ पर वापस",
|
||||||
wikiLinkedChip: "जुड़ा",
|
wikiLinkedChip: "जुड़ा",
|
||||||
wikiRestoredSummary: "पुनर्स्थापित संस्करण",
|
wikiRestoredSummary: "पुनर्स्थापित संस्करण",
|
||||||
wikiExportPdf: "पृष्ठ बनाएँ",
|
|
||||||
wikiPreview: "पूर्वावलोकन",
|
wikiPreview: "पूर्वावलोकन",
|
||||||
tribeSectionWiki: "विकी",
|
tribeSectionWiki: "विकी",
|
||||||
tribeWikiCreate: "विकी बनाएँ",
|
tribeWikiCreate: "विकी बनाएँ",
|
||||||
|
|
@ -3867,6 +3860,9 @@ module.exports = {
|
||||||
roomRecStop: "रिकॉर्डिंग रोकें",
|
roomRecStop: "रिकॉर्डिंग रोकें",
|
||||||
roomStreamFile: "स्ट्रीम करें",
|
roomStreamFile: "स्ट्रीम करें",
|
||||||
roomStreamStop: "रोकें",
|
roomStreamStop: "रोकें",
|
||||||
|
roomScreen: "स्क्रीन",
|
||||||
|
roomScreenSharing: "स्क्रीन साझा हो रही है",
|
||||||
|
roomScreenWarning: "जब तक आप रोकते नहीं, कमरे में सभी आपकी पूरी स्क्रीन देखेंगे।",
|
||||||
roomStreaming: "स्ट्रीम हो रहा है",
|
roomStreaming: "स्ट्रीम हो रहा है",
|
||||||
liveFullscreen: "पूर्ण स्क्रीन",
|
liveFullscreen: "पूर्ण स्क्रीन",
|
||||||
roomStreamPickTitle: "वीडियो चुनें",
|
roomStreamPickTitle: "वीडियो चुनें",
|
||||||
|
|
@ -3924,6 +3920,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "आप पहले से शामिल हैं। आप इसे इसके मॉड्यूल से खोल सकते हैं।",
|
inviteAlreadyMember: "आप पहले से शामिल हैं। आप इसे इसके मॉड्यूल से खोल सकते हैं।",
|
||||||
actionFailed: "यह नहीं हो सका। कृपया फिर से प्रयास करें।",
|
actionFailed: "यह नहीं हो सका। कृपया फिर से प्रयास करें।",
|
||||||
publishTooShort: "आपकी पोस्ट बहुत छोटी है।",
|
publishTooShort: "आपकी पोस्ट बहुत छोटी है।",
|
||||||
|
publishEmpty: "कुछ लिखें या कोई फ़ाइल जोड़ें।",
|
||||||
walletSendFailed: "भुगतान नहीं भेजा जा सका: {reason}",
|
walletSendFailed: "भुगतान नहीं भेजा जा सका: {reason}",
|
||||||
pixeliaCoordsError: "ये निर्देशांक ग्रिड से बाहर हैं।",
|
pixeliaCoordsError: "ये निर्देशांक ग्रिड से बाहर हैं।",
|
||||||
courtsErrTitle: "शीर्षक आवश्यक है।",
|
courtsErrTitle: "शीर्षक आवश्यक है।",
|
||||||
|
|
@ -4435,6 +4432,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "टैग",
|
torrentTagsLabel: "टैग",
|
||||||
torrentTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें",
|
torrentTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें",
|
||||||
torrentSizeLabel: "आकार",
|
torrentSizeLabel: "आकार",
|
||||||
|
torrentInfoHash: "इन्फोहैश",
|
||||||
|
torrentFilesLabel: "फ़ाइलें",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "टोरेंट बनाएँ",
|
torrentFromContent: "टोरेंट बनाएँ",
|
||||||
torrentFromContentMissing: "फ़ाइल अभी इस नोड पर उपलब्ध नहीं है। थोड़ी देर बाद फिर कोशिश करें।",
|
torrentFromContentMissing: "फ़ाइल अभी इस नोड पर उपलब्ध नहीं है। थोड़ी देर बाद फिर कोशिश करें।",
|
||||||
torrentDownloads: "डाउनलोड",
|
torrentDownloads: "डाउनलोड",
|
||||||
|
|
@ -4520,9 +4520,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "स्वीकार",
|
inhabitantsPendingAccept: "स्वीकार",
|
||||||
inhabitantsPendingReject: "अस्वीकार",
|
inhabitantsPendingReject: "अस्वीकार",
|
||||||
tribeSectionGovernance: "शासन",
|
tribeSectionGovernance: "शासन",
|
||||||
tribePadInviteRequired: "पैड तक पहुंच नहीं। निमंत्रण मांगें।",
|
|
||||||
tribeChatInviteRequired: "चैट तक पहुंच नहीं। निमंत्रण मांगें।",
|
|
||||||
tribeGovernanceDesc: "इस जनजाति का आंतरिक शासन।",
|
|
||||||
tribeGovernanceNoGov: "कोई सक्रिय सरकार नहीं",
|
tribeGovernanceNoGov: "कोई सक्रिय सरकार नहीं",
|
||||||
tribeGovernanceNoGovDesc: "इस जनजाति ने अभी सरकार नहीं चुनी।",
|
tribeGovernanceNoGovDesc: "इस जनजाति ने अभी सरकार नहीं चुनी।",
|
||||||
tribeGovCardTitle: "वर्तमान सरकार",
|
tribeGovCardTitle: "वर्तमान सरकार",
|
||||||
|
|
@ -4579,7 +4576,6 @@ module.exports = {
|
||||||
logsFilterMonth: "पिछला महीना",
|
logsFilterMonth: "पिछला महीना",
|
||||||
logsFilterWeek: "पिछला सप्ताह",
|
logsFilterWeek: "पिछला सप्ताह",
|
||||||
logsFilterToday: "आज",
|
logsFilterToday: "आज",
|
||||||
logsComments: "टिप्पणियाँ",
|
|
||||||
logsAuthorEntries: "प्रविष्टियाँ",
|
logsAuthorEntries: "प्रविष्टियाँ",
|
||||||
logsCreate: "प्रविष्टि बनाएँ",
|
logsCreate: "प्रविष्टि बनाएँ",
|
||||||
logsExport: "लॉग निर्यात करें",
|
logsExport: "लॉग निर्यात करें",
|
||||||
|
|
@ -4735,6 +4731,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "WILL परीक्षण",
|
larpWillTestTitle: "WILL परीक्षण",
|
||||||
settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें।",
|
settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें।",
|
||||||
settingsLanEnable: "LAN प्रसारण सक्षम करें",
|
settingsLanEnable: "LAN प्रसारण सक्षम करें",
|
||||||
|
settingsConnectionsTitle: "कनेक्शन",
|
||||||
|
settingsConnectionsDesc: "यह नोड एक साथ कितने पीयर से जुड़ा रहता है।",
|
||||||
settingsLanTitle: "LAN प्रसारण",
|
settingsLanTitle: "LAN प्रसारण",
|
||||||
settingsReplicationTitle: "प्रतिकृति",
|
settingsReplicationTitle: "प्रतिकृति",
|
||||||
settingsReplicationDesc: "सामग्री की प्रतिकृति बनाते समय आपके पीयर द्वारा आपके अपने फ़ीड से आगे जाने वाले हॉप की संख्या कॉन्फ़िगर करें।",
|
settingsReplicationDesc: "सामग्री की प्रतिकृति बनाते समय आपके पीयर द्वारा आपके अपने फ़ीड से आगे जाने वाले हॉप की संख्या कॉन्फ़िगर करें।",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← La mia rete",
|
graphosBackToMine: "← La mia rete",
|
||||||
graphosShowing: "Mostrando",
|
graphosShowing: "Mostrando",
|
||||||
graphosYou: "Tu",
|
graphosYou: "Tu",
|
||||||
graphosTotalNodes: "Nodi totali",
|
graphosTotalNodes: "Totali",
|
||||||
|
onlinePlural: "Online",
|
||||||
mentions: "Menzioni",
|
mentions: "Menzioni",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Ogni messaggio che ti @menziona"),
|
strong("Ogni messaggio che ti @menziona"),
|
||||||
|
|
@ -418,6 +419,7 @@ module.exports = {
|
||||||
menuMedia: "Biblioteca",
|
menuMedia: "Biblioteca",
|
||||||
menuTools: "Strumenti",
|
menuTools: "Strumenti",
|
||||||
comment: "Commento",
|
comment: "Commento",
|
||||||
|
typeComment: "Commento",
|
||||||
subtopic: "Sotto-argomento",
|
subtopic: "Sotto-argomento",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Rimuovi supporto",
|
unfollow: "Rimuovi supporto",
|
||||||
|
|
@ -447,9 +449,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.",
|
"RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -728,10 +727,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Domanda",
|
votesQuestionLabel:"Domanda",
|
||||||
voteStatusOpen:"APERTO",
|
voteStatusOpen:"APERTO",
|
||||||
voteStatusClosed:"CHIUSO",
|
voteStatusClosed:"CHIUSO",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" ha commentato in ",
|
|
||||||
a({ href: parentUrl }, " discussione"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Commento al post di @${authorName}`],
|
commentTitle: ({ authorName }) => [`Commento al post di @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" ha creato un sotto-argomento da ",
|
" ha creato un sotto-argomento da ",
|
||||||
|
|
@ -866,6 +861,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "Il PUB paga nel ciclo successivo e pubblica un trasferimento RBU che puoi confermare.",
|
bankRulesEligibility5: "Il PUB paga nel ciclo successivo e pubblica un trasferimento RBU che puoi confermare.",
|
||||||
bankRulesEligibility6: "Se manca l'indirizzo, il feed è troppo giovane, hai rifiutato il mese o il PUB non ha fondi, la richiesta viene saltata e registrata sul PUB.",
|
bankRulesEligibility6: "Se manca l'indirizzo, il feed è troppo giovane, hai rifiutato il mese o il PUB non ha fondi, la richiesta viene saltata e registrata sul PUB.",
|
||||||
copyLinkTitle: "Copia link",
|
copyLinkTitle: "Copia link",
|
||||||
|
cardMenuTitle: "Altre azioni",
|
||||||
|
shareContent: "Condividi",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Rifiuta il RBU",
|
bankRefuseUBI: "Rifiuta il RBU",
|
||||||
|
|
@ -899,7 +896,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Il tuo portafoglio ECOin non è ancora configurato.",
|
walletNotConfigured: "Il tuo portafoglio ECOin non è ancora configurato.",
|
||||||
walletSettingsDescription: "Integra Oasis con il tuo portafoglio ECOin.",
|
walletSettingsDescription: "Integra Oasis con il tuo portafoglio ECOin.",
|
||||||
walletSettingsMobileDescription: "Collega Oasis all'app ECOin Wallet di questo dispositivo.",
|
walletSettingsMobileDescription: "Collega Oasis all'app ECOin Wallet di questo dispositivo.",
|
||||||
walletDownloadEcoin: "Scarica ECOin!",
|
walletDownloadEcoin: "Scarica ECOin",
|
||||||
walletRpcUrl: "URL RPC",
|
walletRpcUrl: "URL RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Importo non valido",
|
invalid_amount: "Importo non valido",
|
||||||
|
|
@ -952,6 +949,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Aggiorna",
|
bookmarkUpdateButton: "Aggiorna",
|
||||||
bookmarkAddFavoriteButton: "Aggiungi preferito",
|
bookmarkAddFavoriteButton: "Aggiungi preferito",
|
||||||
bookmarkRemoveFavoriteButton: "Rimuovi preferito",
|
bookmarkRemoveFavoriteButton: "Rimuovi preferito",
|
||||||
|
bookmarkTitleLabel: "Titolo",
|
||||||
bookmarkUrlLabel: "Link",
|
bookmarkUrlLabel: "Link",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Descrizione",
|
bookmarkDescriptionLabel: "Descrizione",
|
||||||
|
|
@ -1096,9 +1094,9 @@ module.exports = {
|
||||||
allCVs: "CV",
|
allCVs: "CV",
|
||||||
discoverPeople: "Scopri abitanti nella tua rete.",
|
discoverPeople: "Scopri abitanti nella tua rete.",
|
||||||
searchInhabitantsPlaceholder: "Cerca in Abitanti...",
|
searchInhabitantsPlaceholder: "Cerca in Abitanti...",
|
||||||
filterLocation: "FILTRA abitanti PER POSIZIONE …",
|
filterLocation: "Cerca per posizione…",
|
||||||
filterLanguage: "FILTRA abitanti PER LINGUA …",
|
filterLanguage: "Cerca per lingua…",
|
||||||
filterSkills: "FILTRA abitanti PER COMPETENZE …",
|
filterSkills: "Cerca per competenze…",
|
||||||
cvSkillsCount: "Competenze",
|
cvSkillsCount: "Competenze",
|
||||||
applyFilters: "Applica filtri",
|
applyFilters: "Applica filtri",
|
||||||
locationLabel: "Posizione",
|
locationLabel: "Posizione",
|
||||||
|
|
@ -1114,6 +1112,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Attività",
|
topactivitySectionTitle: "Top Attività",
|
||||||
"TOP INACTIVITYButton": "PIÙ INATTIVI",
|
"TOP INACTIVITYButton": "PIÙ INATTIVI",
|
||||||
topinactivitySectionTitle: "Abitanti meno attivi",
|
topinactivitySectionTitle: "Abitanti meno attivi",
|
||||||
|
contactsSectionTitle: "Contatti",
|
||||||
blockedSectionTitle: "Bloccati",
|
blockedSectionTitle: "Bloccati",
|
||||||
gallerySectionTitle: "GALLERIA",
|
gallerySectionTitle: "GALLERIA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1567,9 +1566,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Data sconosciuta",
|
voteUnknownDate: "Data sconosciuta",
|
||||||
voteDetailSectionTitle: 'Dettagli voto',
|
voteDetailSectionTitle: 'Dettagli voto',
|
||||||
voteCommentsLabel: 'Commenti',
|
voteCommentsLabel: 'Commenti',
|
||||||
voteCommentsForumButton: 'Apri discussione',
|
voteNoCommentsYet: 'Non ci sono ancora commenti.',
|
||||||
voteCommentsSectionTitle: 'Apri discussione',
|
|
||||||
voteNoCommentsYet: 'Non ci sono ancora commenti. Sii il primo a rispondere.',
|
|
||||||
commentsGoToLast: "Vai all'ultimo commento",
|
commentsGoToLast: "Vai all'ultimo commento",
|
||||||
voteNewCommentPlaceholder: 'Scrivi il tuo commento qui…',
|
voteNewCommentPlaceholder: 'Scrivi il tuo commento qui…',
|
||||||
voteNewCommentButton: 'Pubblica commento',
|
voteNewCommentButton: 'Pubblica commento',
|
||||||
|
|
@ -1696,7 +1693,6 @@ module.exports = {
|
||||||
pollVoteButton: "Vota",
|
pollVoteButton: "Vota",
|
||||||
pollChangeVote: "Cambia Voto",
|
pollChangeVote: "Cambia Voto",
|
||||||
pollVoters: "Votanti",
|
pollVoters: "Votanti",
|
||||||
pollComments: "Commenti",
|
|
||||||
pollStatusLabel: "Stato",
|
pollStatusLabel: "Stato",
|
||||||
pollStatusOpen: "APERTO",
|
pollStatusOpen: "APERTO",
|
||||||
pollStatusClosed: "CHIUSO",
|
pollStatusClosed: "CHIUSO",
|
||||||
|
|
@ -1718,9 +1714,22 @@ module.exports = {
|
||||||
blogAllowComments: "Consenti commenti",
|
blogAllowComments: "Consenti commenti",
|
||||||
blogCommentsClosed: "L'autore ha chiuso i commenti di questo blog.",
|
blogCommentsClosed: "L'autore ha chiuso i commenti di questo blog.",
|
||||||
blogNoItems: "Nessun blog trovato.",
|
blogNoItems: "Nessun blog trovato.",
|
||||||
viewBlog: "Vedi Blog",
|
|
||||||
forumTitle: "Forum",
|
forumTitle: "Forum",
|
||||||
forumCategoryLabel: "Categoria",
|
forumCategoryLabel: "Categoria",
|
||||||
|
forumCatGENERAL: "GENERALE",
|
||||||
|
forumCatPOLITICS: "POLITICA",
|
||||||
|
forumCatTECH: "TECNOLOGIA",
|
||||||
|
forumCatSCIENCE: "SCIENZA",
|
||||||
|
forumCatMUSIC: "MUSICA",
|
||||||
|
forumCatART: "ARTE",
|
||||||
|
forumCatGAMING: "GIOCHI",
|
||||||
|
forumCatBOOKS: "LIBRI",
|
||||||
|
forumCatFILMS: "FILM",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFIA",
|
||||||
|
forumCatSOCIETY: "SOCIETÀ",
|
||||||
|
forumCatPRIVACY: "PRIVACY",
|
||||||
|
forumCatCYBERWARFARE: "CYBERGUERRA",
|
||||||
|
forumCatSURVIVALISM: "SOPRAVVIVENZA",
|
||||||
forumTitleLabel: "Titolo",
|
forumTitleLabel: "Titolo",
|
||||||
forumTitlePlaceholder: "Titolo del forum...",
|
forumTitlePlaceholder: "Titolo del forum...",
|
||||||
forumCreateButton: "Crea forum",
|
forumCreateButton: "Crea forum",
|
||||||
|
|
@ -1989,43 +1998,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Segnalazioni in revisione",
|
reportsUnderReviewSectionTitle: "Segnalazioni in revisione",
|
||||||
reportsResolvedSectionTitle: "Segnalazioni risolte",
|
reportsResolvedSectionTitle: "Segnalazioni risolte",
|
||||||
reportsInvalidSectionTitle: "Segnalazioni non valide",
|
reportsInvalidSectionTitle: "Segnalazioni non valide",
|
||||||
reportsBugTemplateTitle: 'Dettagli di riproduzione (Bug)',
|
|
||||||
reportsFeatureTemplateTitle: 'Dettagli (Funzionalità)',
|
|
||||||
reportsAbuseTemplateTitle: 'Dettagli (Abuso)',
|
|
||||||
reportsContentTemplateTitle: 'Dettagli (Problemi di contenuto)',
|
|
||||||
reportsStepsToReproduceLabel: 'Passaggi per riprodurre',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Risultato atteso',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'Cosa dovrebbe succedere?',
|
|
||||||
reportsActualBehaviorLabel: 'Risultato effettivo',
|
|
||||||
reportsActualBehaviorPlaceholder: 'Cosa succede effettivamente?',
|
|
||||||
reportsEnvironmentLabel: 'Ambiente',
|
|
||||||
reportsEnvironmentPlaceholder: 'Versione, dispositivo, SO, browser, impostazioni, log, ecc.',
|
|
||||||
reportsReproduceRateLabel: 'Frequenza di riproduzione',
|
|
||||||
reportsReproduceRateAlways: 'Sempre',
|
|
||||||
reportsReproduceRateOften: 'Spesso',
|
|
||||||
reportsReproduceRateSometimes: 'A volte',
|
|
||||||
reportsReproduceRateRarely: 'Raramente',
|
|
||||||
reportsReproduceRateUnable: 'Impossibile riprodurre',
|
|
||||||
reportsReproduceRateUnknown: 'Non specificato',
|
|
||||||
reportsProblemStatementLabel: 'Problema / necessità',
|
|
||||||
reportsProblemStatementPlaceholder: 'Quale problema risolve questa funzionalità?',
|
|
||||||
reportsUserStoryLabel: 'Storia abitante',
|
|
||||||
reportsUserStoryPlaceholder: 'Come <abitante>, voglio <azione>, così che <beneficio>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Criteri di accettazione',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Dato che...\n- Quando...\n- Allora...',
|
|
||||||
reportsWhatHappenedLabel: 'Cosa è successo?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Descrivi l\'incidente con contesto e date approssimative.',
|
|
||||||
reportsReportedUserLabel: 'Abitante / entità segnalata',
|
|
||||||
reportsReportedUserPlaceholder: '@abitante, feed, ID, link, ecc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Prove / link',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Incolla link, ID messaggi, screenshot (se applicabile), ecc.',
|
|
||||||
reportsContentLocationLabel: 'Dove si trova il contenuto',
|
|
||||||
reportsContentLocationPlaceholder: 'Link, ID, canale, discussione, autore, ecc.',
|
|
||||||
reportsWhyInappropriateLabel: "Perché è inappropriato",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Spiega il motivo e l\'impatto.',
|
|
||||||
reportsRequestedActionLabel: 'Azione richiesta',
|
|
||||||
reportsRequestedActionPlaceholder: 'Rimuovi, nascondi, tagga, avvisa, ecc.',
|
|
||||||
tribesTitle: "Tribù",
|
tribesTitle: "Tribù",
|
||||||
tribeFilterAll: "TUTTE",
|
tribeFilterAll: "TUTTE",
|
||||||
tribeFilterRecent: "RECENTI",
|
tribeFilterRecent: "RECENTI",
|
||||||
|
|
@ -2269,6 +2241,7 @@ module.exports = {
|
||||||
voteWellResearched: "Ben documentato",
|
voteWellResearched: "Ben documentato",
|
||||||
voteNeedsSources: "Servono fonti",
|
voteNeedsSources: "Servono fonti",
|
||||||
voteWrong: "Sbagliato",
|
voteWrong: "Sbagliato",
|
||||||
|
voteOutdated: "Obsoleto",
|
||||||
voteLowQuality: "Bassa qualità",
|
voteLowQuality: "Bassa qualità",
|
||||||
voteLove: "Amore",
|
voteLove: "Amore",
|
||||||
voteClear: "Cancella",
|
voteClear: "Cancella",
|
||||||
|
|
@ -2513,7 +2486,6 @@ module.exports = {
|
||||||
shopNoProducts: "Nessun prodotto ancora.",
|
shopNoProducts: "Nessun prodotto ancora.",
|
||||||
shopOutOfStock: "Esaurito",
|
shopOutOfStock: "Esaurito",
|
||||||
shopBuy: "Acquista",
|
shopBuy: "Acquista",
|
||||||
shopBuyEncryptedNote: "I tuoi dati di consegna vengono inviati cifrati e visibili solo al proprietario del negozio.",
|
|
||||||
shopBuyDeliveryAddress: "Indirizzo di consegna",
|
shopBuyDeliveryAddress: "Indirizzo di consegna",
|
||||||
shopBuyContact: "Contatto",
|
shopBuyContact: "Contatto",
|
||||||
shopBuyContactPlaceholder: "email, telefono, ecc.",
|
shopBuyContactPlaceholder: "email, telefono, ecc.",
|
||||||
|
|
@ -3030,6 +3002,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Descrizione",
|
industryDescriptionLabel: "Descrizione",
|
||||||
industryDescriptionPlaceholder: "Cosa produce questa fabbrica?",
|
industryDescriptionPlaceholder: "Cosa produce questa fabbrica?",
|
||||||
industrySector: "Settore",
|
industrySector: "Settore",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Agricoltura",
|
||||||
|
industrySector_textile: "Tessile",
|
||||||
|
industrySector_energy: "Energia",
|
||||||
|
industrySector_food: "Alimentare",
|
||||||
|
industrySector_construction: "Edilizia",
|
||||||
|
industrySector_media: "Media",
|
||||||
|
industrySector_services: "Servizi",
|
||||||
|
industrySector_other: "Altro",
|
||||||
industryMembershipPolicy: "Politica di adesione",
|
industryMembershipPolicy: "Politica di adesione",
|
||||||
industryQuorum: "Quorum",
|
industryQuorum: "Quorum",
|
||||||
industryMajority: "Maggioranza",
|
industryMajority: "Maggioranza",
|
||||||
|
|
@ -3231,6 +3213,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Modulo per gestire i tuoi contenuti preferiti.",
|
modulesFavoritesDescription: "Modulo per gestire i tuoi contenuti preferiti.",
|
||||||
fileTooLargeTitle: "File troppo grande",
|
fileTooLargeTitle: "File troppo grande",
|
||||||
fileTooLargeMessage: "Il file supera la dimensione massima consentita (75 MB). Seleziona un file più piccolo.",
|
fileTooLargeMessage: "Il file supera la dimensione massima consentita (75 MB). Seleziona un file più piccolo.",
|
||||||
|
fileWrongType: "Questo file non è del tipo giusto per questa sezione.",
|
||||||
|
mediaLockedAfterOpinions: "Ha già delle opinioni: puoi modificare il resto, ma non ciò che è stato valutato.",
|
||||||
|
pdfPrevPage: "Pagina precedente",
|
||||||
|
pdfNextPage: "Pagina successiva",
|
||||||
|
pdfPage: "Pagina",
|
||||||
|
pdfZoomIn: "Ingrandisci",
|
||||||
|
pdfZoomOut: "Riduci",
|
||||||
|
pdfFullscreen: "Schermo intero",
|
||||||
|
pdfClose: "Chiudi",
|
||||||
|
pdfRotate: "Ruota",
|
||||||
|
contentNotAvailableYet: "Questo contenuto non è ancora disponibile.",
|
||||||
|
pdfRetry: "Riprova",
|
||||||
goBack: "Torna indietro",
|
goBack: "Torna indietro",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Si è verificato un errore",
|
errorPageTitle: "Si è verificato un errore",
|
||||||
|
|
@ -3275,8 +3269,6 @@ module.exports = {
|
||||||
courtsRespondentInvalid: "Deve essere un ID SSB valido (@...ed25519)",
|
courtsRespondentInvalid: "Deve essere un ID SSB valido (@...ed25519)",
|
||||||
feedDetailTitle: "Feed",
|
feedDetailTitle: "Feed",
|
||||||
feedOpenDiscussion: "Apri discussione",
|
feedOpenDiscussion: "Apri discussione",
|
||||||
feedPostComment: "Pubblica commento",
|
|
||||||
noComments: "Nessun commento ancora",
|
|
||||||
mapsLabel: "Mappe",
|
mapsLabel: "Mappe",
|
||||||
mapTitle: "Mappe",
|
mapTitle: "Mappe",
|
||||||
mapDescription: "Gestisci e condividi mappe offline nella tua rete.",
|
mapDescription: "Gestisci e condividi mappe offline nella tua rete.",
|
||||||
|
|
@ -3580,6 +3572,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Inserisci tag separati da virgole",
|
podcastTagsPlaceholder: "Inserisci tag separati da virgole",
|
||||||
podcastFilterAll: "TUTTI",
|
podcastFilterAll: "TUTTI",
|
||||||
podcastFilterMine: "MIEI",
|
podcastFilterMine: "MIEI",
|
||||||
|
podcastFilterFavorites: "PREFERITI",
|
||||||
|
podcastEpisodeNumberLabel: "Numero di episodio",
|
||||||
podcastFilterRecent: "RECENTI",
|
podcastFilterRecent: "RECENTI",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "ASCOLTATORI",
|
podcastFilterViewers: "ASCOLTATORI",
|
||||||
|
|
@ -3798,7 +3792,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Torna alla pagina",
|
wikiBackToPage: "Torna alla pagina",
|
||||||
wikiLinkedChip: "Collegata",
|
wikiLinkedChip: "Collegata",
|
||||||
wikiRestoredSummary: "Versione ripristinata",
|
wikiRestoredSummary: "Versione ripristinata",
|
||||||
wikiExportPdf: "Genera pagina",
|
|
||||||
wikiPreview: "Anteprima",
|
wikiPreview: "Anteprima",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Crea wiki",
|
tribeWikiCreate: "Crea wiki",
|
||||||
|
|
@ -3866,6 +3859,9 @@ module.exports = {
|
||||||
roomRecStop: "Ferma la registrazione",
|
roomRecStop: "Ferma la registrazione",
|
||||||
roomStreamFile: "Trasmetti",
|
roomStreamFile: "Trasmetti",
|
||||||
roomStreamStop: "Ferma",
|
roomStreamStop: "Ferma",
|
||||||
|
roomScreen: "Schermo",
|
||||||
|
roomScreenSharing: "Condivisione schermo",
|
||||||
|
roomScreenWarning: "Tutti nella stanza vedranno il tuo schermo intero finché non ti fermi.",
|
||||||
roomStreaming: "In trasmissione",
|
roomStreaming: "In trasmissione",
|
||||||
liveFullscreen: "Schermo intero",
|
liveFullscreen: "Schermo intero",
|
||||||
roomStreamPickTitle: "Scegli un video",
|
roomStreamPickTitle: "Scegli un video",
|
||||||
|
|
@ -3923,6 +3919,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Ne fai già parte. Puoi aprirlo dal suo modulo.",
|
inviteAlreadyMember: "Ne fai già parte. Puoi aprirlo dal suo modulo.",
|
||||||
actionFailed: "Non è stato possibile farlo. Riprova.",
|
actionFailed: "Non è stato possibile farlo. Riprova.",
|
||||||
publishTooShort: "Il tuo post è troppo corto.",
|
publishTooShort: "Il tuo post è troppo corto.",
|
||||||
|
publishEmpty: "Scrivi qualcosa o allega un file.",
|
||||||
walletSendFailed: "Non è stato possibile inviare il pagamento: {reason}",
|
walletSendFailed: "Non è stato possibile inviare il pagamento: {reason}",
|
||||||
pixeliaCoordsError: "Quelle coordinate sono fuori dalla griglia.",
|
pixeliaCoordsError: "Quelle coordinate sono fuori dalla griglia.",
|
||||||
courtsErrTitle: "Serve un titolo.",
|
courtsErrTitle: "Serve un titolo.",
|
||||||
|
|
@ -4434,6 +4431,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Tag",
|
torrentTagsLabel: "Tag",
|
||||||
torrentTagsPlaceholder: "Inserisci tag separati da virgole",
|
torrentTagsPlaceholder: "Inserisci tag separati da virgole",
|
||||||
torrentSizeLabel: "Dimensione",
|
torrentSizeLabel: "Dimensione",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "File",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Genera torrent",
|
torrentFromContent: "Genera torrent",
|
||||||
torrentFromContentMissing: "Il file non è ancora disponibile su questo nodo. Riprova tra un po'.",
|
torrentFromContentMissing: "Il file non è ancora disponibile su questo nodo. Riprova tra un po'.",
|
||||||
torrentDownloads: "Download",
|
torrentDownloads: "Download",
|
||||||
|
|
@ -4519,9 +4519,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Accetta",
|
inhabitantsPendingAccept: "Accetta",
|
||||||
inhabitantsPendingReject: "Rifiuta",
|
inhabitantsPendingReject: "Rifiuta",
|
||||||
tribeSectionGovernance: "GOVERNANCE",
|
tribeSectionGovernance: "GOVERNANCE",
|
||||||
tribePadInviteRequired: "Non hai accesso al pad. Richiedi un invito.",
|
|
||||||
tribeChatInviteRequired: "Non hai accesso alla chat. Richiedi un invito.",
|
|
||||||
tribeGovernanceDesc: "Governance interna di questa tribù.",
|
|
||||||
tribeGovernanceNoGov: "Nessun governo attivo",
|
tribeGovernanceNoGov: "Nessun governo attivo",
|
||||||
tribeGovernanceNoGovDesc: "Questa tribù non ha ancora eletto un governo.",
|
tribeGovernanceNoGovDesc: "Questa tribù non ha ancora eletto un governo.",
|
||||||
tribeGovCardTitle: "Governo Attuale",
|
tribeGovCardTitle: "Governo Attuale",
|
||||||
|
|
@ -4578,7 +4575,6 @@ module.exports = {
|
||||||
logsFilterMonth: "ULTIMO MESE",
|
logsFilterMonth: "ULTIMO MESE",
|
||||||
logsFilterWeek: "ULTIMA SETTIMANA",
|
logsFilterWeek: "ULTIMA SETTIMANA",
|
||||||
logsFilterToday: "OGGI",
|
logsFilterToday: "OGGI",
|
||||||
logsComments: "Commenti",
|
|
||||||
logsAuthorEntries: "Voci",
|
logsAuthorEntries: "Voci",
|
||||||
logsCreate: "Crea voce",
|
logsCreate: "Crea voce",
|
||||||
logsExport: "Esporta diario",
|
logsExport: "Esporta diario",
|
||||||
|
|
@ -4657,6 +4653,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale.",
|
settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale.",
|
||||||
settingsLanEnable: "Abilita trasmissione LAN",
|
settingsLanEnable: "Abilita trasmissione LAN",
|
||||||
|
settingsConnectionsTitle: "Connessioni",
|
||||||
|
settingsConnectionsDesc: "Quanti peer questo nodo mantiene connessi contemporaneamente.",
|
||||||
settingsLanTitle: "Trasmissione LAN",
|
settingsLanTitle: "Trasmissione LAN",
|
||||||
settingsReplicationTitle: "Replicazione",
|
settingsReplicationTitle: "Replicazione",
|
||||||
settingsReplicationDesc: "Configura il numero di salti che il tuo peer segue dal tuo feed durante la replica dei contenuti.",
|
settingsReplicationDesc: "Configura il numero di salti che il tuo peer segue dal tuo feed durante la replica dei contenuti.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Minha rede",
|
graphosBackToMine: "← Minha rede",
|
||||||
graphosShowing: "Mostrando",
|
graphosShowing: "Mostrando",
|
||||||
graphosYou: "Você",
|
graphosYou: "Você",
|
||||||
graphosTotalNodes: "Total de nós",
|
graphosTotalNodes: "Totais",
|
||||||
|
onlinePlural: "Online",
|
||||||
mentions: "Menções",
|
mentions: "Menções",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Todas as mensagens que te @mencionam"),
|
strong("Todas as mensagens que te @mencionam"),
|
||||||
|
|
@ -418,6 +419,7 @@ module.exports = {
|
||||||
menuMedia: "Biblioteca",
|
menuMedia: "Biblioteca",
|
||||||
menuTools: "Ferramentas",
|
menuTools: "Ferramentas",
|
||||||
comment: "Comentário",
|
comment: "Comentário",
|
||||||
|
typeComment: "Comentário",
|
||||||
subtopic: "Subtópico",
|
subtopic: "Subtópico",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Deixar de apoiar",
|
unfollow: "Deixar de apoiar",
|
||||||
|
|
@ -447,9 +449,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.",
|
"LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -728,10 +727,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Pergunta",
|
votesQuestionLabel:"Pergunta",
|
||||||
voteStatusOpen:"ABERTO",
|
voteStatusOpen:"ABERTO",
|
||||||
voteStatusClosed:"FECHADO",
|
voteStatusClosed:"FECHADO",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" comentou em ",
|
|
||||||
a({ href: parentUrl }, " conversa"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Comentário na publicação de @${authorName}`],
|
commentTitle: ({ authorName }) => [`Comentário na publicação de @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" criou um subtópico de ",
|
" criou um subtópico de ",
|
||||||
|
|
@ -866,6 +861,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "O PUB paga no ciclo seguinte e publica um transfer RBU que podes confirmar.",
|
bankRulesEligibility5: "O PUB paga no ciclo seguinte e publica um transfer RBU que podes confirmar.",
|
||||||
bankRulesEligibility6: "Se falta o endereço, o feed é demasiado novo, recusaste o mês ou o PUB não tem fundos, a reclamação é ignorada e registada no PUB.",
|
bankRulesEligibility6: "Se falta o endereço, o feed é demasiado novo, recusaste o mês ou o PUB não tem fundos, a reclamação é ignorada e registada no PUB.",
|
||||||
copyLinkTitle: "Copiar ligação",
|
copyLinkTitle: "Copiar ligação",
|
||||||
|
cardMenuTitle: "Mais ações",
|
||||||
|
shareContent: "Partilhar",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Recusar RBU",
|
bankRefuseUBI: "Recusar RBU",
|
||||||
|
|
@ -899,7 +896,7 @@ module.exports = {
|
||||||
walletNotConfigured: "A sua carteira ECOin ainda não está configurada.",
|
walletNotConfigured: "A sua carteira ECOin ainda não está configurada.",
|
||||||
walletSettingsDescription: "Integra o Oasis com a tua carteira ECOin.",
|
walletSettingsDescription: "Integra o Oasis com a tua carteira ECOin.",
|
||||||
walletSettingsMobileDescription: "Ligue o Oasis à app ECOin Wallet deste dispositivo.",
|
walletSettingsMobileDescription: "Ligue o Oasis à app ECOin Wallet deste dispositivo.",
|
||||||
walletDownloadEcoin: "Descarregar ECOin!",
|
walletDownloadEcoin: "Descarregar ECOin",
|
||||||
walletRpcUrl: "URL do RPC",
|
walletRpcUrl: "URL do RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Montante inválido",
|
invalid_amount: "Montante inválido",
|
||||||
|
|
@ -952,6 +949,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Atualizar",
|
bookmarkUpdateButton: "Atualizar",
|
||||||
bookmarkAddFavoriteButton: "Adicionar favorito",
|
bookmarkAddFavoriteButton: "Adicionar favorito",
|
||||||
bookmarkRemoveFavoriteButton: "Remover favorito",
|
bookmarkRemoveFavoriteButton: "Remover favorito",
|
||||||
|
bookmarkTitleLabel: "Título",
|
||||||
bookmarkUrlLabel: "Link",
|
bookmarkUrlLabel: "Link",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Descrição",
|
bookmarkDescriptionLabel: "Descrição",
|
||||||
|
|
@ -1096,9 +1094,9 @@ module.exports = {
|
||||||
allCVs: "CVs",
|
allCVs: "CVs",
|
||||||
discoverPeople: "Descobre habitantes na tua rede.",
|
discoverPeople: "Descobre habitantes na tua rede.",
|
||||||
searchInhabitantsPlaceholder: "Pesquisar em Habitantes...",
|
searchInhabitantsPlaceholder: "Pesquisar em Habitantes...",
|
||||||
filterLocation: "FILTRAR habitantes POR LOCALIZAÇÃO …",
|
filterLocation: "Pesquisar por localização…",
|
||||||
filterLanguage: "FILTRAR habitantes POR IDIOMA …",
|
filterLanguage: "Pesquisar por idioma…",
|
||||||
filterSkills: "FILTRAR habitantes POR COMPETÊNCIAS …",
|
filterSkills: "Pesquisar por competências…",
|
||||||
cvSkillsCount: "Competências",
|
cvSkillsCount: "Competências",
|
||||||
applyFilters: "Aplicar filtros",
|
applyFilters: "Aplicar filtros",
|
||||||
locationLabel: "Localização",
|
locationLabel: "Localização",
|
||||||
|
|
@ -1114,6 +1112,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Top Atividade",
|
topactivitySectionTitle: "Top Atividade",
|
||||||
"TOP INACTIVITYButton": "MAIS INATIVOS",
|
"TOP INACTIVITYButton": "MAIS INATIVOS",
|
||||||
topinactivitySectionTitle: "Habitantes menos ativos",
|
topinactivitySectionTitle: "Habitantes menos ativos",
|
||||||
|
contactsSectionTitle: "Contactos",
|
||||||
blockedSectionTitle: "Bloqueados",
|
blockedSectionTitle: "Bloqueados",
|
||||||
gallerySectionTitle: "GALERIA",
|
gallerySectionTitle: "GALERIA",
|
||||||
topSectionTitle: "TOP",
|
topSectionTitle: "TOP",
|
||||||
|
|
@ -1567,9 +1566,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Data desconhecida",
|
voteUnknownDate: "Data desconhecida",
|
||||||
voteDetailSectionTitle: 'Detalhes da votação',
|
voteDetailSectionTitle: 'Detalhes da votação',
|
||||||
voteCommentsLabel: 'Comentários',
|
voteCommentsLabel: 'Comentários',
|
||||||
voteCommentsForumButton: 'Abrir discussão',
|
voteNoCommentsYet: 'Ainda sem comentários.',
|
||||||
voteCommentsSectionTitle: 'Abrir discussão',
|
|
||||||
voteNoCommentsYet: 'Ainda sem comentários. Sê o primeiro a responder.',
|
|
||||||
commentsGoToLast: "Ir para o último comentário",
|
commentsGoToLast: "Ir para o último comentário",
|
||||||
voteNewCommentPlaceholder: 'Escreve o teu comentário aqui…',
|
voteNewCommentPlaceholder: 'Escreve o teu comentário aqui…',
|
||||||
voteNewCommentButton: 'Publicar comentário',
|
voteNewCommentButton: 'Publicar comentário',
|
||||||
|
|
@ -1696,7 +1693,6 @@ module.exports = {
|
||||||
pollVoteButton: "Votar",
|
pollVoteButton: "Votar",
|
||||||
pollChangeVote: "Mudar Voto",
|
pollChangeVote: "Mudar Voto",
|
||||||
pollVoters: "Votantes",
|
pollVoters: "Votantes",
|
||||||
pollComments: "Comentários",
|
|
||||||
pollStatusLabel: "Estado",
|
pollStatusLabel: "Estado",
|
||||||
pollStatusOpen: "ABERTA",
|
pollStatusOpen: "ABERTA",
|
||||||
pollStatusClosed: "FECHADA",
|
pollStatusClosed: "FECHADA",
|
||||||
|
|
@ -1718,9 +1714,22 @@ module.exports = {
|
||||||
blogAllowComments: "Permitir comentários",
|
blogAllowComments: "Permitir comentários",
|
||||||
blogCommentsClosed: "O autor fechou os comentários deste blog.",
|
blogCommentsClosed: "O autor fechou os comentários deste blog.",
|
||||||
blogNoItems: "Nenhum blog encontrado.",
|
blogNoItems: "Nenhum blog encontrado.",
|
||||||
viewBlog: "Ver Blog",
|
|
||||||
forumTitle: "Fóruns",
|
forumTitle: "Fóruns",
|
||||||
forumCategoryLabel: "Categoria",
|
forumCategoryLabel: "Categoria",
|
||||||
|
forumCatGENERAL: "GERAL",
|
||||||
|
forumCatPOLITICS: "POLÍTICA",
|
||||||
|
forumCatTECH: "TECNOLOGIA",
|
||||||
|
forumCatSCIENCE: "CIÊNCIA",
|
||||||
|
forumCatMUSIC: "MÚSICA",
|
||||||
|
forumCatART: "ARTE",
|
||||||
|
forumCatGAMING: "JOGOS",
|
||||||
|
forumCatBOOKS: "LIVROS",
|
||||||
|
forumCatFILMS: "FILMES",
|
||||||
|
forumCatPHILOSOPHY: "FILOSOFIA",
|
||||||
|
forumCatSOCIETY: "SOCIEDADE",
|
||||||
|
forumCatPRIVACY: "PRIVACIDADE",
|
||||||
|
forumCatCYBERWARFARE: "CIBERGUERRA",
|
||||||
|
forumCatSURVIVALISM: "SOBREVIVÊNCIA",
|
||||||
forumTitleLabel: "Título",
|
forumTitleLabel: "Título",
|
||||||
forumTitlePlaceholder: "Título do fórum...",
|
forumTitlePlaceholder: "Título do fórum...",
|
||||||
forumCreateButton: "Criar fórum",
|
forumCreateButton: "Criar fórum",
|
||||||
|
|
@ -1989,43 +1998,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Relatórios em análise",
|
reportsUnderReviewSectionTitle: "Relatórios em análise",
|
||||||
reportsResolvedSectionTitle: "Relatórios resolvidos",
|
reportsResolvedSectionTitle: "Relatórios resolvidos",
|
||||||
reportsInvalidSectionTitle: "Relatórios inválidos",
|
reportsInvalidSectionTitle: "Relatórios inválidos",
|
||||||
reportsBugTemplateTitle: 'Detalhes de reprodução (Bugs)',
|
|
||||||
reportsFeatureTemplateTitle: 'Detalhes (Funcionalidades)',
|
|
||||||
reportsAbuseTemplateTitle: 'Detalhes (Abuso)',
|
|
||||||
reportsContentTemplateTitle: 'Detalhes (Problemas de conteúdo)',
|
|
||||||
reportsStepsToReproduceLabel: 'Passos para reproduzir',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: 'Resultado esperado',
|
|
||||||
reportsExpectedBehaviorPlaceholder: 'O que deveria acontecer?',
|
|
||||||
reportsActualBehaviorLabel: 'Resultado real',
|
|
||||||
reportsActualBehaviorPlaceholder: 'O que realmente acontece?',
|
|
||||||
reportsEnvironmentLabel: 'Ambiente',
|
|
||||||
reportsEnvironmentPlaceholder: 'Versão, dispositivo, SO, navegador, definições, registos, etc.',
|
|
||||||
reportsReproduceRateLabel: 'Taxa de reprodução',
|
|
||||||
reportsReproduceRateAlways: 'Sempre',
|
|
||||||
reportsReproduceRateOften: 'Frequentemente',
|
|
||||||
reportsReproduceRateSometimes: 'Às vezes',
|
|
||||||
reportsReproduceRateRarely: 'Raramente',
|
|
||||||
reportsReproduceRateUnable: 'Impossível reproduzir',
|
|
||||||
reportsReproduceRateUnknown: 'Não especificado',
|
|
||||||
reportsProblemStatementLabel: 'Problema / necessidade',
|
|
||||||
reportsProblemStatementPlaceholder: 'Que problema esta funcionalidade resolve?',
|
|
||||||
reportsUserStoryLabel: 'História do habitante',
|
|
||||||
reportsUserStoryPlaceholder: 'Como <habitante>, quero <ação>, para que <benefício>.',
|
|
||||||
reportsAcceptanceCriteriaLabel: 'Critérios de aceitação',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- Dado...\n- Quando...\n- Então...',
|
|
||||||
reportsWhatHappenedLabel: 'O que aconteceu?',
|
|
||||||
reportsWhatHappenedPlaceholder: 'Descreve o incidente com contexto e datas aproximadas.',
|
|
||||||
reportsReportedUserLabel: 'Habitante / entidade denunciada',
|
|
||||||
reportsReportedUserPlaceholder: '@habitante, feed, ID, link, etc.',
|
|
||||||
reportsEvidenceLinksLabel: 'Provas / links',
|
|
||||||
reportsEvidenceLinksPlaceholder: 'Cola links, IDs de mensagens, capturas de ecrã (se aplicável), etc.',
|
|
||||||
reportsContentLocationLabel: 'Onde está o conteúdo',
|
|
||||||
reportsContentLocationPlaceholder: 'Link, ID, canal, conversa, autor, etc.',
|
|
||||||
reportsWhyInappropriateLabel: "Porquê inapropriado",
|
|
||||||
reportsWhyInappropriatePlaceholder: 'Explica o motivo e o impacto.',
|
|
||||||
reportsRequestedActionLabel: 'Ação solicitada',
|
|
||||||
reportsRequestedActionPlaceholder: 'Remover, ocultar, etiquetar, avisar, etc.',
|
|
||||||
tribesTitle: "Tribos",
|
tribesTitle: "Tribos",
|
||||||
tribeFilterAll: "TODAS",
|
tribeFilterAll: "TODAS",
|
||||||
tribeFilterRecent: "RECENTES",
|
tribeFilterRecent: "RECENTES",
|
||||||
|
|
@ -2269,6 +2241,7 @@ module.exports = {
|
||||||
voteWellResearched: "Bem pesquisado",
|
voteWellResearched: "Bem pesquisado",
|
||||||
voteNeedsSources: "Precisa fontes",
|
voteNeedsSources: "Precisa fontes",
|
||||||
voteWrong: "Errado",
|
voteWrong: "Errado",
|
||||||
|
voteOutdated: "Desatualizado",
|
||||||
voteLowQuality: "Baixa qualidade",
|
voteLowQuality: "Baixa qualidade",
|
||||||
voteLove: "Amor",
|
voteLove: "Amor",
|
||||||
voteClear: "Limpar",
|
voteClear: "Limpar",
|
||||||
|
|
@ -2513,7 +2486,6 @@ module.exports = {
|
||||||
shopNoProducts: "Nenhum produto ainda.",
|
shopNoProducts: "Nenhum produto ainda.",
|
||||||
shopOutOfStock: "Esgotado",
|
shopOutOfStock: "Esgotado",
|
||||||
shopBuy: "Comprar",
|
shopBuy: "Comprar",
|
||||||
shopBuyEncryptedNote: "Os teus dados de entrega são enviados cifrados, visíveis apenas para o dono da loja.",
|
|
||||||
shopBuyDeliveryAddress: "Endereço de entrega",
|
shopBuyDeliveryAddress: "Endereço de entrega",
|
||||||
shopBuyContact: "Contacto",
|
shopBuyContact: "Contacto",
|
||||||
shopBuyContactPlaceholder: "email, telefone, etc.",
|
shopBuyContactPlaceholder: "email, telefone, etc.",
|
||||||
|
|
@ -3030,6 +3002,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Descrição",
|
industryDescriptionLabel: "Descrição",
|
||||||
industryDescriptionPlaceholder: "O que esta fábrica produz?",
|
industryDescriptionPlaceholder: "O que esta fábrica produz?",
|
||||||
industrySector: "Setor",
|
industrySector: "Setor",
|
||||||
|
industrySector_software: "Software",
|
||||||
|
industrySector_hardware: "Hardware",
|
||||||
|
industrySector_agriculture: "Agricultura",
|
||||||
|
industrySector_textile: "Têxtil",
|
||||||
|
industrySector_energy: "Energia",
|
||||||
|
industrySector_food: "Alimentação",
|
||||||
|
industrySector_construction: "Construção",
|
||||||
|
industrySector_media: "Media",
|
||||||
|
industrySector_services: "Serviços",
|
||||||
|
industrySector_other: "Outros",
|
||||||
industryMembershipPolicy: "Política de adesão",
|
industryMembershipPolicy: "Política de adesão",
|
||||||
industryQuorum: "Quórum",
|
industryQuorum: "Quórum",
|
||||||
industryMajority: "Maioria",
|
industryMajority: "Maioria",
|
||||||
|
|
@ -3231,6 +3213,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Módulo para gerir o teu conteúdo favorito.",
|
modulesFavoritesDescription: "Módulo para gerir o teu conteúdo favorito.",
|
||||||
fileTooLargeTitle: "Ficheiro demasiado grande",
|
fileTooLargeTitle: "Ficheiro demasiado grande",
|
||||||
fileTooLargeMessage: "O ficheiro excede o tamanho máximo permitido (75 MB). Por favor, seleciona um ficheiro mais pequeno.",
|
fileTooLargeMessage: "O ficheiro excede o tamanho máximo permitido (75 MB). Por favor, seleciona um ficheiro mais pequeno.",
|
||||||
|
fileWrongType: "Este ficheiro não é do tipo adequado para esta secção.",
|
||||||
|
mediaLockedAfterOpinions: "Já tem opiniões: podes editar o resto, mas não mudar o que foi avaliado.",
|
||||||
|
pdfPrevPage: "Página anterior",
|
||||||
|
pdfNextPage: "Página seguinte",
|
||||||
|
pdfPage: "Página",
|
||||||
|
pdfZoomIn: "Aproximar",
|
||||||
|
pdfZoomOut: "Afastar",
|
||||||
|
pdfFullscreen: "Ecrã inteiro",
|
||||||
|
pdfClose: "Fechar",
|
||||||
|
pdfRotate: "Rodar",
|
||||||
|
contentNotAvailableYet: "Este conteúdo ainda não está disponível.",
|
||||||
|
pdfRetry: "Tentar de novo",
|
||||||
goBack: "Voltar",
|
goBack: "Voltar",
|
||||||
errorDismiss: "OK",
|
errorDismiss: "OK",
|
||||||
errorPageTitle: "Algo correu mal",
|
errorPageTitle: "Algo correu mal",
|
||||||
|
|
@ -3275,8 +3269,6 @@ module.exports = {
|
||||||
courtsRespondentInvalid: "Demandado inválido",
|
courtsRespondentInvalid: "Demandado inválido",
|
||||||
feedDetailTitle: "Detalhe do feed",
|
feedDetailTitle: "Detalhe do feed",
|
||||||
feedOpenDiscussion: "Abrir discussão",
|
feedOpenDiscussion: "Abrir discussão",
|
||||||
feedPostComment: "Publicar comentário",
|
|
||||||
noComments: "Ainda sem comentários.",
|
|
||||||
mapsLabel: "Mapas",
|
mapsLabel: "Mapas",
|
||||||
mapTitle: "Mapas",
|
mapTitle: "Mapas",
|
||||||
mapDescription: "Gerencie e compartilhe mapas offline na sua rede.",
|
mapDescription: "Gerencie e compartilhe mapas offline na sua rede.",
|
||||||
|
|
@ -3580,6 +3572,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Introduz tags separadas por vírgulas",
|
podcastTagsPlaceholder: "Introduz tags separadas por vírgulas",
|
||||||
podcastFilterAll: "TODOS",
|
podcastFilterAll: "TODOS",
|
||||||
podcastFilterMine: "MEUS",
|
podcastFilterMine: "MEUS",
|
||||||
|
podcastFilterFavorites: "FAVORITOS",
|
||||||
|
podcastEpisodeNumberLabel: "Número do episódio",
|
||||||
podcastFilterRecent: "RECENTES",
|
podcastFilterRecent: "RECENTES",
|
||||||
podcastFilterTop: "TOP",
|
podcastFilterTop: "TOP",
|
||||||
podcastFilterViewers: "OUVINTES",
|
podcastFilterViewers: "OUVINTES",
|
||||||
|
|
@ -3798,7 +3792,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Voltar à página",
|
wikiBackToPage: "Voltar à página",
|
||||||
wikiLinkedChip: "Ligada",
|
wikiLinkedChip: "Ligada",
|
||||||
wikiRestoredSummary: "Versão restaurada",
|
wikiRestoredSummary: "Versão restaurada",
|
||||||
wikiExportPdf: "Gerar página",
|
|
||||||
wikiPreview: "Pré-visualizar",
|
wikiPreview: "Pré-visualizar",
|
||||||
tribeSectionWiki: "WIKI",
|
tribeSectionWiki: "WIKI",
|
||||||
tribeWikiCreate: "Criar wiki",
|
tribeWikiCreate: "Criar wiki",
|
||||||
|
|
@ -3866,6 +3859,9 @@ module.exports = {
|
||||||
roomRecStop: "Parar a gravação",
|
roomRecStop: "Parar a gravação",
|
||||||
roomStreamFile: "Transmitir",
|
roomStreamFile: "Transmitir",
|
||||||
roomStreamStop: "Parar",
|
roomStreamStop: "Parar",
|
||||||
|
roomScreen: "Ecrã",
|
||||||
|
roomScreenSharing: "Partilhando ecrã",
|
||||||
|
roomScreenWarning: "Todos na sala verão o teu ecrã inteiro até parares.",
|
||||||
roomStreaming: "A transmitir",
|
roomStreaming: "A transmitir",
|
||||||
liveFullscreen: "Ecrã inteiro",
|
liveFullscreen: "Ecrã inteiro",
|
||||||
roomStreamPickTitle: "Escolhe um vídeo",
|
roomStreamPickTitle: "Escolhe um vídeo",
|
||||||
|
|
@ -3923,6 +3919,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Já fazes parte. Podes abri-lo a partir do seu módulo.",
|
inviteAlreadyMember: "Já fazes parte. Podes abri-lo a partir do seu módulo.",
|
||||||
actionFailed: "Não foi possível fazê-lo. Tenta de novo.",
|
actionFailed: "Não foi possível fazê-lo. Tenta de novo.",
|
||||||
publishTooShort: "A tua publicação é demasiado curta.",
|
publishTooShort: "A tua publicação é demasiado curta.",
|
||||||
|
publishEmpty: "Escreve algo ou anexa um ficheiro.",
|
||||||
walletSendFailed: "Não foi possível enviar o pagamento: {reason}",
|
walletSendFailed: "Não foi possível enviar o pagamento: {reason}",
|
||||||
pixeliaCoordsError: "Essas coordenadas estão fora da grelha.",
|
pixeliaCoordsError: "Essas coordenadas estão fora da grelha.",
|
||||||
courtsErrTitle: "É necessário um título.",
|
courtsErrTitle: "É necessário um título.",
|
||||||
|
|
@ -4434,6 +4431,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Tags",
|
torrentTagsLabel: "Tags",
|
||||||
torrentTagsPlaceholder: "Introduz tags separadas por vírgulas",
|
torrentTagsPlaceholder: "Introduz tags separadas por vírgulas",
|
||||||
torrentSizeLabel: "Tamanho",
|
torrentSizeLabel: "Tamanho",
|
||||||
|
torrentInfoHash: "Infohash",
|
||||||
|
torrentFilesLabel: "Ficheiros",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Gerar torrent",
|
torrentFromContent: "Gerar torrent",
|
||||||
torrentFromContentMissing: "O ficheiro ainda não está disponível neste nó. Tenta de novo mais tarde.",
|
torrentFromContentMissing: "O ficheiro ainda não está disponível neste nó. Tenta de novo mais tarde.",
|
||||||
torrentDownloads: "Transferências",
|
torrentDownloads: "Transferências",
|
||||||
|
|
@ -4519,9 +4519,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Aceitar",
|
inhabitantsPendingAccept: "Aceitar",
|
||||||
inhabitantsPendingReject: "Rejeitar",
|
inhabitantsPendingReject: "Rejeitar",
|
||||||
tribeSectionGovernance: "GOVERNANÇA",
|
tribeSectionGovernance: "GOVERNANÇA",
|
||||||
tribePadInviteRequired: "Sem acesso ao pad. Peça um convite.",
|
|
||||||
tribeChatInviteRequired: "Sem acesso ao chat. Peça um convite.",
|
|
||||||
tribeGovernanceDesc: "Governança interna desta tribo.",
|
|
||||||
tribeGovernanceNoGov: "Sem governo ativo",
|
tribeGovernanceNoGov: "Sem governo ativo",
|
||||||
tribeGovernanceNoGovDesc: "Esta tribo ainda não elegeu governo.",
|
tribeGovernanceNoGovDesc: "Esta tribo ainda não elegeu governo.",
|
||||||
tribeGovCardTitle: "Governo Atual",
|
tribeGovCardTitle: "Governo Atual",
|
||||||
|
|
@ -4578,7 +4575,6 @@ module.exports = {
|
||||||
logsFilterMonth: "ÚLTIMO MÊS",
|
logsFilterMonth: "ÚLTIMO MÊS",
|
||||||
logsFilterWeek: "ÚLTIMA SEMANA",
|
logsFilterWeek: "ÚLTIMA SEMANA",
|
||||||
logsFilterToday: "HOJE",
|
logsFilterToday: "HOJE",
|
||||||
logsComments: "Comentários",
|
|
||||||
logsAuthorEntries: "Entradas",
|
logsAuthorEntries: "Entradas",
|
||||||
logsCreate: "Criar entrada",
|
logsCreate: "Criar entrada",
|
||||||
logsExport: "Exportar diário",
|
logsExport: "Exportar diário",
|
||||||
|
|
@ -4672,6 +4668,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "Teste WILL",
|
larpWillTestTitle: "Teste WILL",
|
||||||
settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local.",
|
settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local.",
|
||||||
settingsLanEnable: "Ativar difusão LAN",
|
settingsLanEnable: "Ativar difusão LAN",
|
||||||
|
settingsConnectionsTitle: "Ligações",
|
||||||
|
settingsConnectionsDesc: "Quantos peers este nó mantém ligados ao mesmo tempo.",
|
||||||
settingsLanTitle: "Difusão LAN",
|
settingsLanTitle: "Difusão LAN",
|
||||||
settingsReplicationTitle: "Replicação",
|
settingsReplicationTitle: "Replicação",
|
||||||
settingsReplicationDesc: "Configura o número de saltos que o teu peer segue a partir do teu próprio feed ao replicar conteúdo.",
|
settingsReplicationDesc: "Configura o número de saltos que o teu peer segue a partir do teu próprio feed ao replicar conteúdo.",
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← Моя сеть",
|
graphosBackToMine: "← Моя сеть",
|
||||||
graphosShowing: "Показано",
|
graphosShowing: "Показано",
|
||||||
graphosYou: "Вы",
|
graphosYou: "Вы",
|
||||||
graphosTotalNodes: "Всего узлов",
|
graphosTotalNodes: "Всего",
|
||||||
|
onlinePlural: "В сети",
|
||||||
mentions: "Упоминания",
|
mentions: "Упоминания",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("Каждое сообщение, где вас @упоминают"),
|
strong("Каждое сообщение, где вас @упоминают"),
|
||||||
|
|
@ -429,6 +430,7 @@ module.exports = {
|
||||||
menuMedia: "Библиотека",
|
menuMedia: "Библиотека",
|
||||||
menuTools: "Инструменты",
|
menuTools: "Инструменты",
|
||||||
comment: "Комментарий",
|
comment: "Комментарий",
|
||||||
|
typeComment: "Комментарий",
|
||||||
subtopic: "Подтема",
|
subtopic: "Подтема",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "Отменить поддержку",
|
unfollow: "Отменить поддержку",
|
||||||
|
|
@ -458,9 +460,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.",
|
"ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -739,10 +738,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"Вопрос",
|
votesQuestionLabel:"Вопрос",
|
||||||
voteStatusOpen:"ОТКРЫТО",
|
voteStatusOpen:"ОТКРЫТО",
|
||||||
voteStatusClosed:"ЗАКРЫТО",
|
voteStatusClosed:"ЗАКРЫТО",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" прокомментировал ",
|
|
||||||
a({ href: parentUrl }, " обсуждение"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`Комментарий к записи @${authorName}`],
|
commentTitle: ({ authorName }) => [`Комментарий к записи @${authorName}`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" создал подтему из ",
|
" создал подтему из ",
|
||||||
|
|
@ -877,6 +872,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "PUB платит в следующем цикле и публикует перевод ББД, который вы можете подтвердить.",
|
bankRulesEligibility5: "PUB платит в следующем цикле и публикует перевод ББД, который вы можете подтвердить.",
|
||||||
bankRulesEligibility6: "Если нет адреса, лента слишком молода, месяц отклонён или у PUB нет средств, запрос пропускается и записывается в журнал PUB.",
|
bankRulesEligibility6: "Если нет адреса, лента слишком молода, месяц отклонён или у PUB нет средств, запрос пропускается и записывается в журнал PUB.",
|
||||||
copyLinkTitle: "Скопировать ссылку",
|
copyLinkTitle: "Скопировать ссылку",
|
||||||
|
cardMenuTitle: "Другие действия",
|
||||||
|
shareContent: "Поделиться",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "Отказаться от ББД",
|
bankRefuseUBI: "Отказаться от ББД",
|
||||||
|
|
@ -910,7 +907,7 @@ module.exports = {
|
||||||
walletNotConfigured: "Ваш кошелёк ECOin ещё не настроен.",
|
walletNotConfigured: "Ваш кошелёк ECOin ещё не настроен.",
|
||||||
walletSettingsDescription: "Интегрируйте Oasis с вашим кошельком ECOin.",
|
walletSettingsDescription: "Интегрируйте Oasis с вашим кошельком ECOin.",
|
||||||
walletSettingsMobileDescription: "Свяжите Oasis с приложением ECOin Wallet на этом устройстве.",
|
walletSettingsMobileDescription: "Свяжите Oasis с приложением ECOin Wallet на этом устройстве.",
|
||||||
walletDownloadEcoin: "Скачать ECOin!",
|
walletDownloadEcoin: "Скачать ECOin",
|
||||||
walletRpcUrl: "URL RPC",
|
walletRpcUrl: "URL RPC",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "Неверная сумма",
|
invalid_amount: "Неверная сумма",
|
||||||
|
|
@ -963,6 +960,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "Обновить",
|
bookmarkUpdateButton: "Обновить",
|
||||||
bookmarkAddFavoriteButton: "В избранное",
|
bookmarkAddFavoriteButton: "В избранное",
|
||||||
bookmarkRemoveFavoriteButton: "Из избранного",
|
bookmarkRemoveFavoriteButton: "Из избранного",
|
||||||
|
bookmarkTitleLabel: "Заголовок",
|
||||||
bookmarkUrlLabel: "Ссылка",
|
bookmarkUrlLabel: "Ссылка",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "Описание",
|
bookmarkDescriptionLabel: "Описание",
|
||||||
|
|
@ -1107,9 +1105,9 @@ module.exports = {
|
||||||
allCVs: "Резюме",
|
allCVs: "Резюме",
|
||||||
discoverPeople: "Находите жителей в вашей сети.",
|
discoverPeople: "Находите жителей в вашей сети.",
|
||||||
searchInhabitantsPlaceholder: "Поиск в Жители...",
|
searchInhabitantsPlaceholder: "Поиск в Жители...",
|
||||||
filterLocation: "ФИЛЬТР жителей ПО МЕСТОПОЛОЖЕНИЮ...",
|
filterLocation: "Искать по местоположению…",
|
||||||
filterLanguage: "ФИЛЬТР жителей ПО ЯЗЫКУ...",
|
filterLanguage: "Искать по языку…",
|
||||||
filterSkills: "ФИЛЬТР жителей ПО НАВЫКАМ...",
|
filterSkills: "Искать по навыкам…",
|
||||||
cvSkillsCount: "Навыки",
|
cvSkillsCount: "Навыки",
|
||||||
applyFilters: "Применить фильтры",
|
applyFilters: "Применить фильтры",
|
||||||
locationLabel: "Местоположение",
|
locationLabel: "Местоположение",
|
||||||
|
|
@ -1125,6 +1123,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "Наибольшая активность",
|
topactivitySectionTitle: "Наибольшая активность",
|
||||||
"TOP INACTIVITYButton": "НЕАКТИВНЫЕ",
|
"TOP INACTIVITYButton": "НЕАКТИВНЫЕ",
|
||||||
topinactivitySectionTitle: "Наименее активные жители",
|
topinactivitySectionTitle: "Наименее активные жители",
|
||||||
|
contactsSectionTitle: "Контакты",
|
||||||
blockedSectionTitle: "Заблокированные",
|
blockedSectionTitle: "Заблокированные",
|
||||||
gallerySectionTitle: "ГАЛЕРЕЯ",
|
gallerySectionTitle: "ГАЛЕРЕЯ",
|
||||||
topSectionTitle: "ТОП",
|
topSectionTitle: "ТОП",
|
||||||
|
|
@ -1577,9 +1576,7 @@ module.exports = {
|
||||||
voteUnknownDate: "Неизвестная дата",
|
voteUnknownDate: "Неизвестная дата",
|
||||||
voteDetailSectionTitle: 'Детали голосования',
|
voteDetailSectionTitle: 'Детали голосования',
|
||||||
voteCommentsLabel: 'Комментарии',
|
voteCommentsLabel: 'Комментарии',
|
||||||
voteCommentsForumButton: 'Открыть обсуждение',
|
voteNoCommentsYet: 'Комментариев пока нет.',
|
||||||
voteCommentsSectionTitle: 'Открыть обсуждение',
|
|
||||||
voteNoCommentsYet: 'Комментариев пока нет. Будьте первым.',
|
|
||||||
commentsGoToLast: "Перейти к последнему комментарию",
|
commentsGoToLast: "Перейти к последнему комментарию",
|
||||||
voteNewCommentPlaceholder: 'Напишите ваш комментарий...',
|
voteNewCommentPlaceholder: 'Напишите ваш комментарий...',
|
||||||
voteNewCommentButton: 'Отправить комментарий',
|
voteNewCommentButton: 'Отправить комментарий',
|
||||||
|
|
@ -1706,7 +1703,6 @@ module.exports = {
|
||||||
pollVoteButton: "Голосовать",
|
pollVoteButton: "Голосовать",
|
||||||
pollChangeVote: "Изменить голос",
|
pollChangeVote: "Изменить голос",
|
||||||
pollVoters: "Проголосовали",
|
pollVoters: "Проголосовали",
|
||||||
pollComments: "Комментарии",
|
|
||||||
pollStatusLabel: "Статус",
|
pollStatusLabel: "Статус",
|
||||||
pollStatusOpen: "ОТКРЫТ",
|
pollStatusOpen: "ОТКРЫТ",
|
||||||
pollStatusClosed: "ЗАКРЫТ",
|
pollStatusClosed: "ЗАКРЫТ",
|
||||||
|
|
@ -1728,9 +1724,22 @@ module.exports = {
|
||||||
blogAllowComments: "Разрешить комментарии",
|
blogAllowComments: "Разрешить комментарии",
|
||||||
blogCommentsClosed: "Автор закрыл комментарии к этому блогу.",
|
blogCommentsClosed: "Автор закрыл комментарии к этому блогу.",
|
||||||
blogNoItems: "Блоги не найдены.",
|
blogNoItems: "Блоги не найдены.",
|
||||||
viewBlog: "Смотреть блог",
|
|
||||||
forumTitle: "Форумы",
|
forumTitle: "Форумы",
|
||||||
forumCategoryLabel: "Категория",
|
forumCategoryLabel: "Категория",
|
||||||
|
forumCatGENERAL: "ОБЩЕЕ",
|
||||||
|
forumCatPOLITICS: "ПОЛИТИКА",
|
||||||
|
forumCatTECH: "ТЕХНОЛОГИИ",
|
||||||
|
forumCatSCIENCE: "НАУКА",
|
||||||
|
forumCatMUSIC: "МУЗЫКА",
|
||||||
|
forumCatART: "ИСКУССТВО",
|
||||||
|
forumCatGAMING: "ИГРЫ",
|
||||||
|
forumCatBOOKS: "КНИГИ",
|
||||||
|
forumCatFILMS: "ФИЛЬМЫ",
|
||||||
|
forumCatPHILOSOPHY: "ФИЛОСОФИЯ",
|
||||||
|
forumCatSOCIETY: "ОБЩЕСТВО",
|
||||||
|
forumCatPRIVACY: "ПРИВАТНОСТЬ",
|
||||||
|
forumCatCYBERWARFARE: "КИБЕРВОЙНА",
|
||||||
|
forumCatSURVIVALISM: "ВЫЖИВАНИЕ",
|
||||||
forumTitleLabel: "Название",
|
forumTitleLabel: "Название",
|
||||||
forumTitlePlaceholder: "Название форума...",
|
forumTitlePlaceholder: "Название форума...",
|
||||||
forumCreateButton: "Создать форум",
|
forumCreateButton: "Создать форум",
|
||||||
|
|
@ -2005,43 +2014,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "Отчёты на рассмотрении",
|
reportsUnderReviewSectionTitle: "Отчёты на рассмотрении",
|
||||||
reportsResolvedSectionTitle: "Решённые отчёты",
|
reportsResolvedSectionTitle: "Решённые отчёты",
|
||||||
reportsInvalidSectionTitle: "Недействительные отчёты",
|
reportsInvalidSectionTitle: "Недействительные отчёты",
|
||||||
reportsBugTemplateTitle: "Детали воспроизведения (Ошибки)",
|
|
||||||
reportsFeatureTemplateTitle: "Детали (Функции)",
|
|
||||||
reportsAbuseTemplateTitle: "Детали (Злоупотребления)",
|
|
||||||
reportsContentTemplateTitle: "Детали (Проблемы с контентом)",
|
|
||||||
reportsStepsToReproduceLabel: "Шаги воспроизведения",
|
|
||||||
reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...",
|
|
||||||
reportsExpectedBehaviorLabel: "Ожидаемый результат",
|
|
||||||
reportsExpectedBehaviorPlaceholder: "Что должно произойти?",
|
|
||||||
reportsActualBehaviorLabel: "Фактический результат",
|
|
||||||
reportsActualBehaviorPlaceholder: "Что произошло на самом деле?",
|
|
||||||
reportsEnvironmentLabel: "Среда",
|
|
||||||
reportsEnvironmentPlaceholder: "Версия, устройство, ОС, браузер, настройки, журналы и т.д.",
|
|
||||||
reportsReproduceRateLabel: "Частота воспроизведения",
|
|
||||||
reportsReproduceRateAlways: "Всегда",
|
|
||||||
reportsReproduceRateOften: "Часто",
|
|
||||||
reportsReproduceRateSometimes: "Иногда",
|
|
||||||
reportsReproduceRateRarely: "Редко",
|
|
||||||
reportsReproduceRateUnable: "Невозможно воспроизвести",
|
|
||||||
reportsReproduceRateUnknown: "Не указано",
|
|
||||||
reportsProblemStatementLabel: "Проблема / потребность",
|
|
||||||
reportsProblemStatementPlaceholder: "Какую проблему решает эта функция?",
|
|
||||||
reportsUserStoryLabel: "История жителя",
|
|
||||||
reportsUserStoryPlaceholder: "Как <житель>, я хочу <действие>, чтобы <польза>.",
|
|
||||||
reportsAcceptanceCriteriaLabel: "Критерии приёмки",
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: "- Дано...\n- Когда...\n- Тогда...",
|
|
||||||
reportsWhatHappenedLabel: "Что произошло?",
|
|
||||||
reportsWhatHappenedPlaceholder: "Опишите инцидент с контекстом и примерными датами.",
|
|
||||||
reportsReportedUserLabel: "Сообщаемый житель / сущность",
|
|
||||||
reportsReportedUserPlaceholder: "@житель, лента, ID, ссылка и т.д.",
|
|
||||||
reportsEvidenceLinksLabel: "Доказательства / ссылки",
|
|
||||||
reportsEvidenceLinksPlaceholder: "Вставьте ссылки, ID сообщений, скриншоты (если применимо) и т.д.",
|
|
||||||
reportsContentLocationLabel: "Где находится контент",
|
|
||||||
reportsContentLocationPlaceholder: "Ссылка, ID, канал, тред, автор и т.д.",
|
|
||||||
reportsWhyInappropriateLabel: "Почему это неуместно",
|
|
||||||
reportsWhyInappropriatePlaceholder: "Объясните причину и последствия.",
|
|
||||||
reportsRequestedActionLabel: "Запрашиваемое действие",
|
|
||||||
reportsRequestedActionPlaceholder: "Удалить, скрыть, пометить, предупредить и т.д.",
|
|
||||||
tribesTitle: "Племена",
|
tribesTitle: "Племена",
|
||||||
tribeFilterAll: "ВСЕ",
|
tribeFilterAll: "ВСЕ",
|
||||||
tribeFilterRecent: "НЕДАВНИЕ",
|
tribeFilterRecent: "НЕДАВНИЕ",
|
||||||
|
|
@ -2282,6 +2254,7 @@ module.exports = {
|
||||||
voteWellResearched: "Хорошо исследованное",
|
voteWellResearched: "Хорошо исследованное",
|
||||||
voteNeedsSources: "Нужны источники",
|
voteNeedsSources: "Нужны источники",
|
||||||
voteWrong: "Неправильное",
|
voteWrong: "Неправильное",
|
||||||
|
voteOutdated: "Устарело",
|
||||||
voteLowQuality: "Низкое качество",
|
voteLowQuality: "Низкое качество",
|
||||||
voteLove: "Люблю",
|
voteLove: "Люблю",
|
||||||
voteClear: "Ясное",
|
voteClear: "Ясное",
|
||||||
|
|
@ -2515,7 +2488,6 @@ module.exports = {
|
||||||
shopNoProducts: "Товаров пока нет.",
|
shopNoProducts: "Товаров пока нет.",
|
||||||
shopOutOfStock: "Нет в наличии",
|
shopOutOfStock: "Нет в наличии",
|
||||||
shopBuy: "Купить",
|
shopBuy: "Купить",
|
||||||
shopBuyEncryptedNote: "Ваши данные доставки отправляются зашифрованно и видны только владельцу магазина.",
|
|
||||||
shopBuyDeliveryAddress: "Адрес доставки",
|
shopBuyDeliveryAddress: "Адрес доставки",
|
||||||
shopBuyContact: "Контакт",
|
shopBuyContact: "Контакт",
|
||||||
shopBuyContactPlaceholder: "email, телефон и т.д.",
|
shopBuyContactPlaceholder: "email, телефон и т.д.",
|
||||||
|
|
@ -3027,6 +2999,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "Описание",
|
industryDescriptionLabel: "Описание",
|
||||||
industryDescriptionPlaceholder: "Что производит эта фабрика?",
|
industryDescriptionPlaceholder: "Что производит эта фабрика?",
|
||||||
industrySector: "Сектор",
|
industrySector: "Сектор",
|
||||||
|
industrySector_software: "Программы",
|
||||||
|
industrySector_hardware: "Оборудование",
|
||||||
|
industrySector_agriculture: "Сельское хозяйство",
|
||||||
|
industrySector_textile: "Текстиль",
|
||||||
|
industrySector_energy: "Энергетика",
|
||||||
|
industrySector_food: "Продовольствие",
|
||||||
|
industrySector_construction: "Строительство",
|
||||||
|
industrySector_media: "Медиа",
|
||||||
|
industrySector_services: "Услуги",
|
||||||
|
industrySector_other: "Другое",
|
||||||
industryMembershipPolicy: "Политика членства",
|
industryMembershipPolicy: "Политика членства",
|
||||||
industryQuorum: "Кворум",
|
industryQuorum: "Кворум",
|
||||||
industryMajority: "Большинство",
|
industryMajority: "Большинство",
|
||||||
|
|
@ -3228,6 +3210,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "Модуль для управления избранным контентом.",
|
modulesFavoritesDescription: "Модуль для управления избранным контентом.",
|
||||||
fileTooLargeTitle: "Файл слишком большой",
|
fileTooLargeTitle: "Файл слишком большой",
|
||||||
fileTooLargeMessage: "Файл превышает максимально допустимый размер (50 МБ). Пожалуйста, выберите файл меньшего размера.",
|
fileTooLargeMessage: "Файл превышает максимально допустимый размер (50 МБ). Пожалуйста, выберите файл меньшего размера.",
|
||||||
|
fileWrongType: "Этот файл не подходит для этого раздела по типу.",
|
||||||
|
mediaLockedAfterOpinions: "У этого уже есть мнения: остальное можно править, но не то, что оценивали.",
|
||||||
|
pdfPrevPage: "Предыдущая страница",
|
||||||
|
pdfNextPage: "Следующая страница",
|
||||||
|
pdfPage: "Страница",
|
||||||
|
pdfZoomIn: "Увеличить",
|
||||||
|
pdfZoomOut: "Уменьшить",
|
||||||
|
pdfFullscreen: "Полный экран",
|
||||||
|
pdfClose: "Закрыть",
|
||||||
|
pdfRotate: "Повернуть",
|
||||||
|
contentNotAvailableYet: "Этот контент пока недоступен.",
|
||||||
|
pdfRetry: "Повторить",
|
||||||
goBack: "Назад",
|
goBack: "Назад",
|
||||||
errorDismiss: "ОК",
|
errorDismiss: "ОК",
|
||||||
errorPageTitle: "Что-то пошло не так",
|
errorPageTitle: "Что-то пошло не так",
|
||||||
|
|
@ -3270,9 +3264,7 @@ module.exports = {
|
||||||
uploadMedia: "Загрузить медиа (макс: 75MB)",
|
uploadMedia: "Загрузить медиа (макс: 75MB)",
|
||||||
feedOpenDiscussion: "Открыть обсуждение",
|
feedOpenDiscussion: "Открыть обсуждение",
|
||||||
feedDetailTitle: "Лента",
|
feedDetailTitle: "Лента",
|
||||||
feedPostComment: "Опубликовать комментарий",
|
|
||||||
courtsRespondentInvalid: "Должен быть действительным SSB ID (@...ed25519)",
|
courtsRespondentInvalid: "Должен быть действительным SSB ID (@...ed25519)",
|
||||||
noComments: "Пока нет комментариев",
|
|
||||||
bankAllocId: "ID распределения",
|
bankAllocId: "ID распределения",
|
||||||
pmFromLabel: "От:",
|
pmFromLabel: "От:",
|
||||||
pmToLabel: "Кому:",
|
pmToLabel: "Кому:",
|
||||||
|
|
@ -3581,6 +3573,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "Введите теги через запятую",
|
podcastTagsPlaceholder: "Введите теги через запятую",
|
||||||
podcastFilterAll: "ВСЕ",
|
podcastFilterAll: "ВСЕ",
|
||||||
podcastFilterMine: "МОИ",
|
podcastFilterMine: "МОИ",
|
||||||
|
podcastFilterFavorites: "ИЗБРАННОЕ",
|
||||||
|
podcastEpisodeNumberLabel: "Номер выпуска",
|
||||||
podcastFilterRecent: "НЕДАВНИЕ",
|
podcastFilterRecent: "НЕДАВНИЕ",
|
||||||
podcastFilterTop: "ТОП",
|
podcastFilterTop: "ТОП",
|
||||||
podcastFilterViewers: "СЛУШАТЕЛИ",
|
podcastFilterViewers: "СЛУШАТЕЛИ",
|
||||||
|
|
@ -3799,7 +3793,6 @@ module.exports = {
|
||||||
wikiBackToPage: "Назад к странице",
|
wikiBackToPage: "Назад к странице",
|
||||||
wikiLinkedChip: "Связана",
|
wikiLinkedChip: "Связана",
|
||||||
wikiRestoredSummary: "Восстановленная версия",
|
wikiRestoredSummary: "Восстановленная версия",
|
||||||
wikiExportPdf: "Создать страницу",
|
|
||||||
wikiPreview: "Предпросмотр",
|
wikiPreview: "Предпросмотр",
|
||||||
tribeSectionWiki: "ВИКИ",
|
tribeSectionWiki: "ВИКИ",
|
||||||
tribeWikiCreate: "Создать вики",
|
tribeWikiCreate: "Создать вики",
|
||||||
|
|
@ -3867,6 +3860,9 @@ module.exports = {
|
||||||
roomRecStop: "Остановить запись",
|
roomRecStop: "Остановить запись",
|
||||||
roomStreamFile: "Транслировать",
|
roomStreamFile: "Транслировать",
|
||||||
roomStreamStop: "Остановить",
|
roomStreamStop: "Остановить",
|
||||||
|
roomScreen: "Экран",
|
||||||
|
roomScreenSharing: "Демонстрация экрана",
|
||||||
|
roomScreenWarning: "Все в комнате будут видеть ваш экран целиком, пока вы не остановите.",
|
||||||
roomStreaming: "Идёт трансляция",
|
roomStreaming: "Идёт трансляция",
|
||||||
liveFullscreen: "Во весь экран",
|
liveFullscreen: "Во весь экран",
|
||||||
roomStreamPickTitle: "Выберите видео",
|
roomStreamPickTitle: "Выберите видео",
|
||||||
|
|
@ -3924,6 +3920,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "Вы уже участвуете. Откройте его в соответствующем модуле.",
|
inviteAlreadyMember: "Вы уже участвуете. Откройте его в соответствующем модуле.",
|
||||||
actionFailed: "Не удалось это сделать. Попробуйте ещё раз.",
|
actionFailed: "Не удалось это сделать. Попробуйте ещё раз.",
|
||||||
publishTooShort: "Ваша публикация слишком короткая.",
|
publishTooShort: "Ваша публикация слишком короткая.",
|
||||||
|
publishEmpty: "Напишите что-нибудь или прикрепите файл.",
|
||||||
walletSendFailed: "Не удалось отправить платёж: {reason}",
|
walletSendFailed: "Не удалось отправить платёж: {reason}",
|
||||||
pixeliaCoordsError: "Эти координаты находятся за пределами сетки.",
|
pixeliaCoordsError: "Эти координаты находятся за пределами сетки.",
|
||||||
courtsErrTitle: "Нужен заголовок.",
|
courtsErrTitle: "Нужен заголовок.",
|
||||||
|
|
@ -4435,6 +4432,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "Теги",
|
torrentTagsLabel: "Теги",
|
||||||
torrentTagsPlaceholder: "Введите теги через запятую",
|
torrentTagsPlaceholder: "Введите теги через запятую",
|
||||||
torrentSizeLabel: "Размер",
|
torrentSizeLabel: "Размер",
|
||||||
|
torrentInfoHash: "Инфохеш",
|
||||||
|
torrentFilesLabel: "Файлы",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "Создать торрент",
|
torrentFromContent: "Создать торрент",
|
||||||
torrentFromContentMissing: "Файл ещё недоступен на этом узле. Попробуйте позже.",
|
torrentFromContentMissing: "Файл ещё недоступен на этом узле. Попробуйте позже.",
|
||||||
torrentDownloads: "Загрузки",
|
torrentDownloads: "Загрузки",
|
||||||
|
|
@ -4520,9 +4520,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "Принять",
|
inhabitantsPendingAccept: "Принять",
|
||||||
inhabitantsPendingReject: "Отклонить",
|
inhabitantsPendingReject: "Отклонить",
|
||||||
tribeSectionGovernance: "УПРАВЛЕНИЕ",
|
tribeSectionGovernance: "УПРАВЛЕНИЕ",
|
||||||
tribePadInviteRequired: "Нет доступа к паду. Запросите приглашение.",
|
|
||||||
tribeChatInviteRequired: "Нет доступа к чату. Запросите приглашение.",
|
|
||||||
tribeGovernanceDesc: "Внутреннее управление этим племенем.",
|
|
||||||
tribeGovernanceNoGov: "Нет активного правительства",
|
tribeGovernanceNoGov: "Нет активного правительства",
|
||||||
tribeGovernanceNoGovDesc: "Это племя ещё не выбрало правительство.",
|
tribeGovernanceNoGovDesc: "Это племя ещё не выбрало правительство.",
|
||||||
tribeGovCardTitle: "Текущее правительство",
|
tribeGovCardTitle: "Текущее правительство",
|
||||||
|
|
@ -4579,7 +4576,6 @@ module.exports = {
|
||||||
logsFilterMonth: "МЕСЯЦ",
|
logsFilterMonth: "МЕСЯЦ",
|
||||||
logsFilterWeek: "НЕДЕЛЯ",
|
logsFilterWeek: "НЕДЕЛЯ",
|
||||||
logsFilterToday: "СЕГОДНЯ",
|
logsFilterToday: "СЕГОДНЯ",
|
||||||
logsComments: "Комментарии",
|
|
||||||
logsAuthorEntries: "Записи",
|
logsAuthorEntries: "Записи",
|
||||||
logsCreate: "Создать запись",
|
logsCreate: "Создать запись",
|
||||||
logsExport: "Экспорт журнала",
|
logsExport: "Экспорт журнала",
|
||||||
|
|
@ -4735,6 +4731,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "WILL-тест",
|
larpWillTestTitle: "WILL-тест",
|
||||||
settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети.",
|
settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети.",
|
||||||
settingsLanEnable: "Включить LAN-вещание",
|
settingsLanEnable: "Включить LAN-вещание",
|
||||||
|
settingsConnectionsTitle: "Подключения",
|
||||||
|
settingsConnectionsDesc: "Сколько пиров этот узел держит подключёнными одновременно.",
|
||||||
settingsLanTitle: "LAN-вещание",
|
settingsLanTitle: "LAN-вещание",
|
||||||
settingsReplicationTitle: "Репликация",
|
settingsReplicationTitle: "Репликация",
|
||||||
settingsReplicationDesc: "Настройте количество переходов, которые ваш узел делает от вашего собственного канала при репликации содержимого.",
|
settingsReplicationDesc: "Настройте количество переходов, которые ваш узел делает от вашего собственного канала при репликации содержимого.",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,8 @@ module.exports = {
|
||||||
graphosBackToMine: "← 我的网络",
|
graphosBackToMine: "← 我的网络",
|
||||||
graphosShowing: "显示",
|
graphosShowing: "显示",
|
||||||
graphosYou: "你",
|
graphosYou: "你",
|
||||||
graphosTotalNodes: "节点总数",
|
graphosTotalNodes: "总计",
|
||||||
|
onlinePlural: "在线",
|
||||||
mentions: "提及",
|
mentions: "提及",
|
||||||
mentionsDescription: [
|
mentionsDescription: [
|
||||||
strong("每一条 @提到你的消息"),
|
strong("每一条 @提到你的消息"),
|
||||||
|
|
@ -420,6 +421,7 @@ module.exports = {
|
||||||
menuMedia: "资料库",
|
menuMedia: "资料库",
|
||||||
menuTools: "工具",
|
menuTools: "工具",
|
||||||
comment: "评论",
|
comment: "评论",
|
||||||
|
typeComment: "评论",
|
||||||
subtopic: "子话题",
|
subtopic: "子话题",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
unfollow: "取消支持",
|
unfollow: "取消支持",
|
||||||
|
|
@ -449,9 +451,6 @@ module.exports = {
|
||||||
publishCustomDescription: [
|
publishCustomDescription: [
|
||||||
"提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。",
|
"提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。",
|
||||||
],
|
],
|
||||||
commentWarning: [
|
|
||||||
"提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。",
|
|
||||||
],
|
|
||||||
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
commentLabel: ({ publicOrPrivate, markdownUrl }) => [
|
||||||
],
|
],
|
||||||
publishLabel: ({ markdownUrl, linkTarget }) => [
|
publishLabel: ({ markdownUrl, linkTarget }) => [
|
||||||
|
|
@ -730,10 +729,6 @@ module.exports = {
|
||||||
votesQuestionLabel:"问题",
|
votesQuestionLabel:"问题",
|
||||||
voteStatusOpen:"进行中",
|
voteStatusOpen:"进行中",
|
||||||
voteStatusClosed:"已关闭",
|
voteStatusClosed:"已关闭",
|
||||||
commentDescription: ({ parentUrl }) => [
|
|
||||||
" 评论了 ",
|
|
||||||
a({ href: parentUrl }, " 讨论串"),
|
|
||||||
],
|
|
||||||
commentTitle: ({ authorName }) => [`对 @${authorName} 帖子的评论`],
|
commentTitle: ({ authorName }) => [`对 @${authorName} 帖子的评论`],
|
||||||
subtopicDescription: ({ parentUrl }) => [
|
subtopicDescription: ({ parentUrl }) => [
|
||||||
" 从 ",
|
" 从 ",
|
||||||
|
|
@ -869,6 +864,8 @@ module.exports = {
|
||||||
bankRulesEligibility5: "PUB 在下一周期支付并发布一笔可供确认的 UBI 转账。",
|
bankRulesEligibility5: "PUB 在下一周期支付并发布一笔可供确认的 UBI 转账。",
|
||||||
bankRulesEligibility6: "若缺少地址、feed 太新、已拒绝当月或 PUB 无资金,领取将被跳过并记录在 PUB 上。",
|
bankRulesEligibility6: "若缺少地址、feed 太新、已拒绝当月或 PUB 无资金,领取将被跳过并记录在 PUB 上。",
|
||||||
copyLinkTitle: "复制链接",
|
copyLinkTitle: "复制链接",
|
||||||
|
cardMenuTitle: "更多操作",
|
||||||
|
shareContent: "分享",
|
||||||
linkBoxOasis: "Oasis",
|
linkBoxOasis: "Oasis",
|
||||||
linkBoxClearnet: "Clearnet",
|
linkBoxClearnet: "Clearnet",
|
||||||
bankRefuseUBI: "拒绝 UBI",
|
bankRefuseUBI: "拒绝 UBI",
|
||||||
|
|
@ -902,7 +899,7 @@ module.exports = {
|
||||||
walletNotConfigured: "你的 ECOin 钱包尚未配置。",
|
walletNotConfigured: "你的 ECOin 钱包尚未配置。",
|
||||||
walletSettingsDescription: "将 Oasis 与你的 ECOin 钱包集成。",
|
walletSettingsDescription: "将 Oasis 与你的 ECOin 钱包集成。",
|
||||||
walletSettingsMobileDescription: "将 Oasis 与此设备上的 ECOin Wallet 应用关联。",
|
walletSettingsMobileDescription: "将 Oasis 与此设备上的 ECOin Wallet 应用关联。",
|
||||||
walletDownloadEcoin: "下载 ECOin!",
|
walletDownloadEcoin: "下载 ECOin",
|
||||||
walletRpcUrl: "RPC 地址",
|
walletRpcUrl: "RPC 地址",
|
||||||
walletStatusMessages: {
|
walletStatusMessages: {
|
||||||
invalid_amount: "无效金额",
|
invalid_amount: "无效金额",
|
||||||
|
|
@ -955,6 +952,7 @@ module.exports = {
|
||||||
bookmarkUpdateButton: "更新",
|
bookmarkUpdateButton: "更新",
|
||||||
bookmarkAddFavoriteButton: "添加收藏",
|
bookmarkAddFavoriteButton: "添加收藏",
|
||||||
bookmarkRemoveFavoriteButton: "取消收藏",
|
bookmarkRemoveFavoriteButton: "取消收藏",
|
||||||
|
bookmarkTitleLabel: "标题",
|
||||||
bookmarkUrlLabel: "链接",
|
bookmarkUrlLabel: "链接",
|
||||||
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
bookmarkUrlPlaceholder: "https://solarnethub.com",
|
||||||
bookmarkDescriptionLabel: "描述",
|
bookmarkDescriptionLabel: "描述",
|
||||||
|
|
@ -1099,9 +1097,9 @@ module.exports = {
|
||||||
allCVs: "简历",
|
allCVs: "简历",
|
||||||
discoverPeople: "发现你网络中的居民。",
|
discoverPeople: "发现你网络中的居民。",
|
||||||
searchInhabitantsPlaceholder: "在居民中搜索...",
|
searchInhabitantsPlaceholder: "在居民中搜索...",
|
||||||
filterLocation: "按位置筛选居民...",
|
filterLocation: "按位置搜索…",
|
||||||
filterLanguage: "按语言筛选居民...",
|
filterLanguage: "按语言搜索…",
|
||||||
filterSkills: "按技能筛选居民...",
|
filterSkills: "按技能搜索…",
|
||||||
cvSkillsCount: "技能",
|
cvSkillsCount: "技能",
|
||||||
applyFilters: "应用筛选",
|
applyFilters: "应用筛选",
|
||||||
locationLabel: "位置",
|
locationLabel: "位置",
|
||||||
|
|
@ -1117,6 +1115,7 @@ module.exports = {
|
||||||
topactivitySectionTitle: "最高活跃度",
|
topactivitySectionTitle: "最高活跃度",
|
||||||
"TOP INACTIVITYButton": "最不活跃",
|
"TOP INACTIVITYButton": "最不活跃",
|
||||||
topinactivitySectionTitle: "最不活跃的居民",
|
topinactivitySectionTitle: "最不活跃的居民",
|
||||||
|
contactsSectionTitle: "联系人",
|
||||||
blockedSectionTitle: "已屏蔽",
|
blockedSectionTitle: "已屏蔽",
|
||||||
gallerySectionTitle: "画廊",
|
gallerySectionTitle: "画廊",
|
||||||
topSectionTitle: "排行",
|
topSectionTitle: "排行",
|
||||||
|
|
@ -1570,9 +1569,7 @@ module.exports = {
|
||||||
voteUnknownDate: "未知日期",
|
voteUnknownDate: "未知日期",
|
||||||
voteDetailSectionTitle: '投票详情',
|
voteDetailSectionTitle: '投票详情',
|
||||||
voteCommentsLabel: '评论',
|
voteCommentsLabel: '评论',
|
||||||
voteCommentsForumButton: '公开讨论',
|
voteNoCommentsYet: '还没有评论。',
|
||||||
voteCommentsSectionTitle: '公开讨论',
|
|
||||||
voteNoCommentsYet: '还没有评论。成为第一个回复的人。',
|
|
||||||
commentsGoToLast: "跳到最后一条评论",
|
commentsGoToLast: "跳到最后一条评论",
|
||||||
voteNewCommentPlaceholder: '在此写下你的评论…',
|
voteNewCommentPlaceholder: '在此写下你的评论…',
|
||||||
voteNewCommentButton: '发表评论',
|
voteNewCommentButton: '发表评论',
|
||||||
|
|
@ -1699,7 +1696,6 @@ module.exports = {
|
||||||
pollVoteButton: "投票",
|
pollVoteButton: "投票",
|
||||||
pollChangeVote: "修改投票",
|
pollChangeVote: "修改投票",
|
||||||
pollVoters: "投票人",
|
pollVoters: "投票人",
|
||||||
pollComments: "评论",
|
|
||||||
pollStatusLabel: "状态",
|
pollStatusLabel: "状态",
|
||||||
pollStatusOpen: "进行中",
|
pollStatusOpen: "进行中",
|
||||||
pollStatusClosed: "已结束",
|
pollStatusClosed: "已结束",
|
||||||
|
|
@ -1721,9 +1717,22 @@ module.exports = {
|
||||||
blogAllowComments: "允许评论",
|
blogAllowComments: "允许评论",
|
||||||
blogCommentsClosed: "作者已关闭此博客的评论。",
|
blogCommentsClosed: "作者已关闭此博客的评论。",
|
||||||
blogNoItems: "未找到博客。",
|
blogNoItems: "未找到博客。",
|
||||||
viewBlog: "查看博客",
|
|
||||||
forumTitle: "论坛",
|
forumTitle: "论坛",
|
||||||
forumCategoryLabel: "类别",
|
forumCategoryLabel: "类别",
|
||||||
|
forumCatGENERAL: "综合",
|
||||||
|
forumCatPOLITICS: "政治",
|
||||||
|
forumCatTECH: "科技",
|
||||||
|
forumCatSCIENCE: "科学",
|
||||||
|
forumCatMUSIC: "音乐",
|
||||||
|
forumCatART: "艺术",
|
||||||
|
forumCatGAMING: "游戏",
|
||||||
|
forumCatBOOKS: "书籍",
|
||||||
|
forumCatFILMS: "电影",
|
||||||
|
forumCatPHILOSOPHY: "哲学",
|
||||||
|
forumCatSOCIETY: "社会",
|
||||||
|
forumCatPRIVACY: "隐私",
|
||||||
|
forumCatCYBERWARFARE: "网络战",
|
||||||
|
forumCatSURVIVALISM: "生存",
|
||||||
forumTitleLabel: "标题",
|
forumTitleLabel: "标题",
|
||||||
forumTitlePlaceholder: "论坛标题...",
|
forumTitlePlaceholder: "论坛标题...",
|
||||||
forumCreateButton: "创建论坛",
|
forumCreateButton: "创建论坛",
|
||||||
|
|
@ -1992,43 +2001,6 @@ module.exports = {
|
||||||
reportsUnderReviewSectionTitle: "审查中的报告",
|
reportsUnderReviewSectionTitle: "审查中的报告",
|
||||||
reportsResolvedSectionTitle: "已解决的报告",
|
reportsResolvedSectionTitle: "已解决的报告",
|
||||||
reportsInvalidSectionTitle: "无效报告",
|
reportsInvalidSectionTitle: "无效报告",
|
||||||
reportsBugTemplateTitle: '复现详情(漏洞)',
|
|
||||||
reportsFeatureTemplateTitle: '详情(功能)',
|
|
||||||
reportsAbuseTemplateTitle: '详情(滥用)',
|
|
||||||
reportsContentTemplateTitle: '详情(内容问题)',
|
|
||||||
reportsStepsToReproduceLabel: '复现步骤',
|
|
||||||
reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...',
|
|
||||||
reportsExpectedBehaviorLabel: '预期结果',
|
|
||||||
reportsExpectedBehaviorPlaceholder: '应该发生什么?',
|
|
||||||
reportsActualBehaviorLabel: '实际结果',
|
|
||||||
reportsActualBehaviorPlaceholder: '实际发生了什么?',
|
|
||||||
reportsEnvironmentLabel: '环境',
|
|
||||||
reportsEnvironmentPlaceholder: '版本、设备、操作系统、浏览器、设置、日志等。',
|
|
||||||
reportsReproduceRateLabel: '复现率',
|
|
||||||
reportsReproduceRateAlways: '总是',
|
|
||||||
reportsReproduceRateOften: '经常',
|
|
||||||
reportsReproduceRateSometimes: '有时',
|
|
||||||
reportsReproduceRateRarely: '偶尔',
|
|
||||||
reportsReproduceRateUnable: '无法复现',
|
|
||||||
reportsReproduceRateUnknown: '未指定',
|
|
||||||
reportsProblemStatementLabel: '问题/需求',
|
|
||||||
reportsProblemStatementPlaceholder: '此功能解决什么问题?',
|
|
||||||
reportsUserStoryLabel: '居民故事',
|
|
||||||
reportsUserStoryPlaceholder: '作为一个 <居民>,我希望 <动作>,以便 <收益>。',
|
|
||||||
reportsAcceptanceCriteriaLabel: '验收标准',
|
|
||||||
reportsAcceptanceCriteriaPlaceholder: '- 假设...\n- 当...\n- 那么...',
|
|
||||||
reportsWhatHappenedLabel: '发生了什么?',
|
|
||||||
reportsWhatHappenedPlaceholder: '描述事件的背景和大致日期。',
|
|
||||||
reportsReportedUserLabel: '被举报的居民/实体',
|
|
||||||
reportsReportedUserPlaceholder: '@居民、动态、ID、链接等。',
|
|
||||||
reportsEvidenceLinksLabel: '证据/链接',
|
|
||||||
reportsEvidenceLinksPlaceholder: '粘贴链接、消息 ID、截图(如适用)等。',
|
|
||||||
reportsContentLocationLabel: '内容位置',
|
|
||||||
reportsContentLocationPlaceholder: '链接、ID、频道、讨论串、作者等。',
|
|
||||||
reportsWhyInappropriateLabel: "为什么不合适",
|
|
||||||
reportsWhyInappropriatePlaceholder: '说明原因和影响。',
|
|
||||||
reportsRequestedActionLabel: '请求的操作',
|
|
||||||
reportsRequestedActionPlaceholder: '删除、隐藏、标记、警告等。',
|
|
||||||
tribesTitle: "部落",
|
tribesTitle: "部落",
|
||||||
tribeFilterAll: "全部",
|
tribeFilterAll: "全部",
|
||||||
tribeFilterRecent: "最近",
|
tribeFilterRecent: "最近",
|
||||||
|
|
@ -2272,6 +2244,7 @@ module.exports = {
|
||||||
voteWellResearched: "研究充分",
|
voteWellResearched: "研究充分",
|
||||||
voteNeedsSources: "需要来源",
|
voteNeedsSources: "需要来源",
|
||||||
voteWrong: "错误",
|
voteWrong: "错误",
|
||||||
|
voteOutdated: "已过时",
|
||||||
voteLowQuality: "低质量",
|
voteLowQuality: "低质量",
|
||||||
voteLove: "喜爱",
|
voteLove: "喜爱",
|
||||||
voteClear: "清晰",
|
voteClear: "清晰",
|
||||||
|
|
@ -2516,7 +2489,6 @@ module.exports = {
|
||||||
shopNoProducts: "暂无产品。",
|
shopNoProducts: "暂无产品。",
|
||||||
shopOutOfStock: "已售罄",
|
shopOutOfStock: "已售罄",
|
||||||
shopBuy: "购买",
|
shopBuy: "购买",
|
||||||
shopBuyEncryptedNote: "您的配送信息将加密发送,仅店主可见。",
|
|
||||||
shopBuyDeliveryAddress: "配送地址",
|
shopBuyDeliveryAddress: "配送地址",
|
||||||
shopBuyContact: "联系方式",
|
shopBuyContact: "联系方式",
|
||||||
shopBuyContactPlaceholder: "邮箱、电话等",
|
shopBuyContactPlaceholder: "邮箱、电话等",
|
||||||
|
|
@ -3033,6 +3005,16 @@ module.exports = {
|
||||||
industryDescriptionLabel: "描述",
|
industryDescriptionLabel: "描述",
|
||||||
industryDescriptionPlaceholder: "这家工厂生产什么?",
|
industryDescriptionPlaceholder: "这家工厂生产什么?",
|
||||||
industrySector: "部门",
|
industrySector: "部门",
|
||||||
|
industrySector_software: "软件",
|
||||||
|
industrySector_hardware: "硬件",
|
||||||
|
industrySector_agriculture: "农业",
|
||||||
|
industrySector_textile: "纺织",
|
||||||
|
industrySector_energy: "能源",
|
||||||
|
industrySector_food: "食品",
|
||||||
|
industrySector_construction: "建筑",
|
||||||
|
industrySector_media: "媒体",
|
||||||
|
industrySector_services: "服务",
|
||||||
|
industrySector_other: "其他",
|
||||||
industryMembershipPolicy: "成员政策",
|
industryMembershipPolicy: "成员政策",
|
||||||
industryQuorum: "法定人数",
|
industryQuorum: "法定人数",
|
||||||
industryMajority: "多数",
|
industryMajority: "多数",
|
||||||
|
|
@ -3234,6 +3216,18 @@ module.exports = {
|
||||||
modulesFavoritesDescription: "管理你收藏内容的模块。",
|
modulesFavoritesDescription: "管理你收藏内容的模块。",
|
||||||
fileTooLargeTitle: "文件过大",
|
fileTooLargeTitle: "文件过大",
|
||||||
fileTooLargeMessage: "文件超过了允许的最大大小(75 MB)。请选择较小的文件。",
|
fileTooLargeMessage: "文件超过了允许的最大大小(75 MB)。请选择较小的文件。",
|
||||||
|
fileWrongType: "此文件类型不适用于此栏目。",
|
||||||
|
mediaLockedAfterOpinions: "已有评价:可以编辑其他内容,但不能更改已被评价的部分。",
|
||||||
|
pdfPrevPage: "上一页",
|
||||||
|
pdfNextPage: "下一页",
|
||||||
|
pdfPage: "页",
|
||||||
|
pdfZoomIn: "放大",
|
||||||
|
pdfZoomOut: "缩小",
|
||||||
|
pdfFullscreen: "全屏",
|
||||||
|
pdfClose: "关闭",
|
||||||
|
pdfRotate: "旋转",
|
||||||
|
contentNotAvailableYet: "此内容暂不可用。",
|
||||||
|
pdfRetry: "重试",
|
||||||
goBack: "返回",
|
goBack: "返回",
|
||||||
errorDismiss: "好",
|
errorDismiss: "好",
|
||||||
errorPageTitle: "出现了问题",
|
errorPageTitle: "出现了问题",
|
||||||
|
|
@ -3276,9 +3270,7 @@ module.exports = {
|
||||||
uploadMedia: "上传媒体(最大:75MB)",
|
uploadMedia: "上传媒体(最大:75MB)",
|
||||||
feedOpenDiscussion: "公开讨论",
|
feedOpenDiscussion: "公开讨论",
|
||||||
feedDetailTitle: "动态",
|
feedDetailTitle: "动态",
|
||||||
feedPostComment: "发表评论",
|
|
||||||
courtsRespondentInvalid: "必须是有效的 SSB ID (@...ed25519)",
|
courtsRespondentInvalid: "必须是有效的 SSB ID (@...ed25519)",
|
||||||
noComments: "还没有评论",
|
|
||||||
mapsLabel: "地图",
|
mapsLabel: "地图",
|
||||||
mapTitle: "地图",
|
mapTitle: "地图",
|
||||||
mapDescription: "在您的网络中管理和分享离线地图。",
|
mapDescription: "在您的网络中管理和分享离线地图。",
|
||||||
|
|
@ -3582,6 +3574,8 @@ module.exports = {
|
||||||
podcastTagsPlaceholder: "输入标签,用逗号分隔",
|
podcastTagsPlaceholder: "输入标签,用逗号分隔",
|
||||||
podcastFilterAll: "全部",
|
podcastFilterAll: "全部",
|
||||||
podcastFilterMine: "我的",
|
podcastFilterMine: "我的",
|
||||||
|
podcastFilterFavorites: "收藏",
|
||||||
|
podcastEpisodeNumberLabel: "集数",
|
||||||
podcastFilterRecent: "最近",
|
podcastFilterRecent: "最近",
|
||||||
podcastFilterTop: "热门",
|
podcastFilterTop: "热门",
|
||||||
podcastFilterViewers: "听众",
|
podcastFilterViewers: "听众",
|
||||||
|
|
@ -3800,7 +3794,6 @@ module.exports = {
|
||||||
wikiBackToPage: "返回页面",
|
wikiBackToPage: "返回页面",
|
||||||
wikiLinkedChip: "已关联",
|
wikiLinkedChip: "已关联",
|
||||||
wikiRestoredSummary: "已恢复的版本",
|
wikiRestoredSummary: "已恢复的版本",
|
||||||
wikiExportPdf: "生成页面",
|
|
||||||
wikiPreview: "预览",
|
wikiPreview: "预览",
|
||||||
tribeSectionWiki: "维基",
|
tribeSectionWiki: "维基",
|
||||||
tribeWikiCreate: "创建维基",
|
tribeWikiCreate: "创建维基",
|
||||||
|
|
@ -3868,6 +3861,9 @@ module.exports = {
|
||||||
roomRecStop: "停止录音",
|
roomRecStop: "停止录音",
|
||||||
roomStreamFile: "播放",
|
roomStreamFile: "播放",
|
||||||
roomStreamStop: "停止",
|
roomStreamStop: "停止",
|
||||||
|
roomScreen: "屏幕",
|
||||||
|
roomScreenSharing: "正在共享屏幕",
|
||||||
|
roomScreenWarning: "在你停止之前,房间里的每个人都会看到你的整个屏幕。",
|
||||||
roomStreaming: "正在播放",
|
roomStreaming: "正在播放",
|
||||||
liveFullscreen: "全屏",
|
liveFullscreen: "全屏",
|
||||||
roomStreamPickTitle: "选择视频",
|
roomStreamPickTitle: "选择视频",
|
||||||
|
|
@ -3925,6 +3921,7 @@ module.exports = {
|
||||||
inviteAlreadyMember: "你已经加入了,可以在对应模块中打开。",
|
inviteAlreadyMember: "你已经加入了,可以在对应模块中打开。",
|
||||||
actionFailed: "无法完成此操作,请重试。",
|
actionFailed: "无法完成此操作,请重试。",
|
||||||
publishTooShort: "你的帖子太短了。",
|
publishTooShort: "你的帖子太短了。",
|
||||||
|
publishEmpty: "请写点内容或附加一个文件。",
|
||||||
walletSendFailed: "无法发送付款:{reason}",
|
walletSendFailed: "无法发送付款:{reason}",
|
||||||
pixeliaCoordsError: "这些坐标超出了网格范围。",
|
pixeliaCoordsError: "这些坐标超出了网格范围。",
|
||||||
courtsErrTitle: "需要填写标题。",
|
courtsErrTitle: "需要填写标题。",
|
||||||
|
|
@ -4436,6 +4433,9 @@ module.exports = {
|
||||||
torrentTagsLabel: "标签",
|
torrentTagsLabel: "标签",
|
||||||
torrentTagsPlaceholder: "输入标签,用逗号分隔",
|
torrentTagsPlaceholder: "输入标签,用逗号分隔",
|
||||||
torrentSizeLabel: "大小",
|
torrentSizeLabel: "大小",
|
||||||
|
torrentInfoHash: "信息哈希",
|
||||||
|
torrentFilesLabel: "文件",
|
||||||
|
torrentMagnet: "MAGNET",
|
||||||
torrentFromContent: "生成种子",
|
torrentFromContent: "生成种子",
|
||||||
torrentFromContentMissing: "该文件在本节点尚不可用,请稍后再试。",
|
torrentFromContentMissing: "该文件在本节点尚不可用,请稍后再试。",
|
||||||
torrentDownloads: "下载",
|
torrentDownloads: "下载",
|
||||||
|
|
@ -4521,9 +4521,6 @@ module.exports = {
|
||||||
inhabitantsPendingAccept: "接受",
|
inhabitantsPendingAccept: "接受",
|
||||||
inhabitantsPendingReject: "拒绝",
|
inhabitantsPendingReject: "拒绝",
|
||||||
tribeSectionGovernance: "治理",
|
tribeSectionGovernance: "治理",
|
||||||
tribePadInviteRequired: "您无权访问 pad。请求邀请。",
|
|
||||||
tribeChatInviteRequired: "您无权访问聊天。请求邀请。",
|
|
||||||
tribeGovernanceDesc: "此部落的内部治理。",
|
|
||||||
tribeGovernanceNoGov: "无活跃政府",
|
tribeGovernanceNoGov: "无活跃政府",
|
||||||
tribeGovernanceNoGovDesc: "此部落尚未选举政府。",
|
tribeGovernanceNoGovDesc: "此部落尚未选举政府。",
|
||||||
tribeGovCardTitle: "当前政府",
|
tribeGovCardTitle: "当前政府",
|
||||||
|
|
@ -4580,7 +4577,6 @@ module.exports = {
|
||||||
logsFilterMonth: "上月",
|
logsFilterMonth: "上月",
|
||||||
logsFilterWeek: "上周",
|
logsFilterWeek: "上周",
|
||||||
logsFilterToday: "今天",
|
logsFilterToday: "今天",
|
||||||
logsComments: "评论",
|
|
||||||
logsAuthorEntries: "条目",
|
logsAuthorEntries: "条目",
|
||||||
logsCreate: "创建条目",
|
logsCreate: "创建条目",
|
||||||
logsExport: "导出日志",
|
logsExport: "导出日志",
|
||||||
|
|
@ -4736,6 +4732,8 @@ module.exports = {
|
||||||
larpWillTestTitle: "WILL 测试",
|
larpWillTestTitle: "WILL 测试",
|
||||||
settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。",
|
settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。",
|
||||||
settingsLanEnable: "启用局域网广播",
|
settingsLanEnable: "启用局域网广播",
|
||||||
|
settingsConnectionsTitle: "连接",
|
||||||
|
settingsConnectionsDesc: "此节点同时保持连接的节点数量。",
|
||||||
settingsLanTitle: "局域网广播",
|
settingsLanTitle: "局域网广播",
|
||||||
settingsReplicationTitle: "复制",
|
settingsReplicationTitle: "复制",
|
||||||
settingsReplicationDesc: "配置在复制内容时,你的节点从自己的信息流出发跟随的跳数。",
|
settingsReplicationDesc: "配置在复制内容时,你的节点从自己的信息流出发跟随的跳数。",
|
||||||
|
|
|
||||||
|
|
@ -115,13 +115,13 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false }
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (internalSSB) {
|
if (internalSSB) {
|
||||||
const { printMetadata, colors } = require('../server/ssb_metadata');
|
const { printMetadata, colors } = require('../server/ssb_metadata');
|
||||||
printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic);
|
printMetadata('OASIS UX', colors.yellow, port, host, isPublic);
|
||||||
return resolve(windowLogStream(internalSSB));
|
return resolve(windowLogStream(internalSSB));
|
||||||
}
|
}
|
||||||
if (internalOpen) {
|
if (internalOpen) {
|
||||||
return internalOpen().then((srv) => {
|
return internalOpen().then((srv) => {
|
||||||
const { printMetadata, colors } = require('../server/ssb_metadata');
|
const { printMetadata, colors } = require('../server/ssb_metadata');
|
||||||
printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic);
|
printMetadata('OASIS UX', colors.yellow, port, host, isPublic);
|
||||||
resolve(windowLogStream(srv));
|
resolve(windowLogStream(srv));
|
||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +138,7 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false }
|
||||||
reject(new Error("Closing Oasis"));
|
reject(new Error("Closing Oasis"));
|
||||||
} else {
|
} else {
|
||||||
const { printMetadata, colors } = require('../server/ssb_metadata');
|
const { printMetadata, colors } = require('../server/ssb_metadata');
|
||||||
printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic);
|
printMetadata('OASIS UX', colors.yellow, port, host, isPublic);
|
||||||
resolve(ssb);
|
resolve(ssb);
|
||||||
}
|
}
|
||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,19 @@ module.exports = ({ host, port, middleware, allowHost }) => {
|
||||||
|
|
||||||
const { isClearnetPath, isTrustedRequest, buildCsp } = require(path.join(__dirname, "..", "backend", "request_guards"));
|
const { isClearnetPath, isTrustedRequest, buildCsp } = require(path.join(__dirname, "..", "backend", "request_guards"));
|
||||||
const isValidRequest = (request) => isTrustedRequest(request, validHosts);
|
const isValidRequest = (request) => isTrustedRequest(request, validHosts);
|
||||||
|
const EMBED_PATHS = ["/pdf/embed"];
|
||||||
|
const CLEARNET_EMBED_PATHS = ["/c/pdf/embed"];
|
||||||
|
const pageFrames = (ctx, isClearnet) => {
|
||||||
|
if (ctx.method !== "GET") return {};
|
||||||
|
const host = String(ctx.host || '');
|
||||||
|
const embeds = /^[A-Za-z0-9.-]+(:\d+)?$/.test(host) ? (isClearnet ? CLEARNET_EMBED_PATHS : EMBED_PATHS).map(p => `${host}${p}`).join(' ') : "'self'";
|
||||||
|
const game = /^\/games\/[a-z0-9]+\/?$/.test(ctx.path);
|
||||||
|
return {
|
||||||
|
objects: embeds,
|
||||||
|
frames: game ? "'self'" : embeds,
|
||||||
|
framed: EMBED_PATHS.includes(ctx.path) || CLEARNET_EMBED_PATHS.includes(ctx.path)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
||||||
|
|
||||||
|
|
@ -92,15 +105,13 @@ module.exports = ({ host, port, middleware, allowHost }) => {
|
||||||
gamesStatic.use(koaStatic(join(__dirname, "..", "games")));
|
gamesStatic.use(koaStatic(join(__dirname, "..", "games")));
|
||||||
app.use(mount("/game-assets", gamesStatic));
|
app.use(mount("/game-assets", gamesStatic));
|
||||||
|
|
||||||
app.use(mount("/js", koaStatic(path.join(__dirname, 'public/js'))));
|
|
||||||
app.use(koaStatic(path.join(__dirname, 'public')));
|
|
||||||
|
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
|
|
||||||
if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`);
|
if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`);
|
||||||
|
|
||||||
const isClearnet = isClearnetPath(ctx.request);
|
const isClearnet = isClearnetPath(ctx.request);
|
||||||
ctx.set("Content-Security-Policy", buildCsp(isClearnet, { frames: ctx.method === "GET" && /^\/games\/[a-z0-9]+\/?$/.test(ctx.path) }));
|
ctx.set("Content-Security-Policy", buildCsp(isClearnet, pageFrames(ctx, isClearnet)));
|
||||||
ctx.set("X-Frame-Options", "SAMEORIGIN");
|
ctx.set("X-Frame-Options", "SAMEORIGIN");
|
||||||
|
|
||||||
ctx.set("X-Content-Type-Options", "nosniff");
|
ctx.set("X-Content-Type-Options", "nosniff");
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ PUB admin commands (require the Oasis Sbot to be running):
|
||||||
GUI options (forwarded to the backend):
|
GUI options (forwarded to the backend):
|
||||||
--host=<ip> Hostname / IP the web UI listens on (default: localhost).
|
--host=<ip> Hostname / IP the web UI listens on (default: localhost).
|
||||||
Use 0.0.0.0 to expose on a VPS.
|
Use 0.0.0.0 to expose on a VPS.
|
||||||
--port=<n> Port for the web UI (default: 3000).
|
--port=<n> Port for the web UI (default: 3000, or OASIS_PORT).
|
||||||
--allow-host=<host> Extra hostname allowed when behind a reverse proxy.
|
--allow-host=<host> Extra hostname allowed when behind a reverse proxy.
|
||||||
--public Public-hosting mode: disables POST and redacts content
|
--public Public-hosting mode: disables POST and redacts content
|
||||||
from people who haven't opted in to public hosting.
|
from people who haven't opted in to public hosting.
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
const showPdfFallback = () => {
|
|
||||||
document.querySelectorAll('.pdf-viewer-container').forEach(container => {
|
|
||||||
if (container.dataset.pdfReady) return;
|
|
||||||
container.dataset.pdfReady = '1';
|
|
||||||
container.textContent = '';
|
|
||||||
const url = container.getAttribute('data-pdf-url');
|
|
||||||
if (url) {
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.textContent = '📄 PDF';
|
|
||||||
container.appendChild(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPdfViewers = async () => {
|
|
||||||
let pdfjsLib;
|
|
||||||
try {
|
|
||||||
pdfjsLib = await import('/js/pdf.min.mjs?v=102');
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.mjs?v=102';
|
|
||||||
} catch (err) {
|
|
||||||
showPdfFallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.pdf-viewer-container').forEach(async container => {
|
|
||||||
const pdfUrl = container.getAttribute('data-pdf-url');
|
|
||||||
if (!pdfUrl || container.dataset.pdfReady) return;
|
|
||||||
container.dataset.pdfReady = '1';
|
|
||||||
|
|
||||||
let pdf;
|
|
||||||
try {
|
|
||||||
pdf = await pdfjsLib.getDocument(pdfUrl).promise;
|
|
||||||
} catch (err) {
|
|
||||||
container.textContent = '';
|
|
||||||
const fallback = document.createElement('a');
|
|
||||||
fallback.href = pdfUrl;
|
|
||||||
fallback.textContent = '📄 PDF';
|
|
||||||
container.appendChild(fallback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPage = 1;
|
|
||||||
let scale = 1.5;
|
|
||||||
let rotation = 0;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const canvasWrap = document.createElement('div');
|
|
||||||
canvasWrap.className = 'pdf-canvas-wrap';
|
|
||||||
canvasWrap.appendChild(canvas);
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.appendChild(canvasWrap);
|
|
||||||
|
|
||||||
const controls = document.createElement('div');
|
|
||||||
controls.className = 'pdf-controls';
|
|
||||||
controls.innerHTML = `
|
|
||||||
<button class="pdf-prev">⬅️</button>
|
|
||||||
<button class="pdf-next">➡️</button>
|
|
||||||
<button class="pdf-zoom-in">🔍+</button>
|
|
||||||
<button class="pdf-zoom-out">🔍−</button>
|
|
||||||
<button class="pdf-rotate">↻</button>
|
|
||||||
<button class="pdf-download">⬇️</button>
|
|
||||||
<button class="pdf-fullscreen">🔲</button>
|
|
||||||
<button class="pdf-metadata">ℹ️</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(controls);
|
|
||||||
|
|
||||||
const renderPage = async (num) => {
|
|
||||||
const page = await pdf.getPage(num);
|
|
||||||
const viewport = page.getViewport({ scale, rotation });
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToPage = (delta) => {
|
|
||||||
const newPage = currentPage + delta;
|
|
||||||
if (newPage >= 1 && newPage <= pdf.numPages) {
|
|
||||||
currentPage = newPage;
|
|
||||||
renderPage(currentPage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const firstPage = await pdf.getPage(1);
|
|
||||||
const baseViewport = firstPage.getViewport({ scale: 1, rotation: 0 });
|
|
||||||
const avail = canvasWrap.clientWidth || container.clientWidth;
|
|
||||||
if (avail > 0 && baseViewport.width > 0) {
|
|
||||||
scale = Math.min(1.5, Math.max(0.5, (avail - 40) / baseViewport.width));
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
renderPage(currentPage);
|
|
||||||
|
|
||||||
controls.querySelector('.pdf-prev').onclick = () => goToPage(-1);
|
|
||||||
controls.querySelector('.pdf-next').onclick = () => goToPage(1);
|
|
||||||
controls.querySelector('.pdf-zoom-in').onclick = () => { scale += 0.2; renderPage(currentPage); };
|
|
||||||
controls.querySelector('.pdf-zoom-out').onclick = () => { scale = Math.max(0.5, scale - 0.2); renderPage(currentPage); };
|
|
||||||
controls.querySelector('.pdf-rotate').onclick = () => { rotation = (rotation + 90) % 360; renderPage(currentPage); };
|
|
||||||
controls.querySelector('.pdf-download').onclick = () => {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = pdfUrl;
|
|
||||||
a.download = 'document.pdf';
|
|
||||||
a.click();
|
|
||||||
};
|
|
||||||
controls.querySelector('.pdf-fullscreen').onclick = async () => {
|
|
||||||
const maximized = container.classList.toggle('pdf-maximized');
|
|
||||||
document.body.classList.toggle('pdf-maximized-body', maximized);
|
|
||||||
try {
|
|
||||||
const p1 = await pdf.getPage(currentPage);
|
|
||||||
const vw1 = p1.getViewport({ scale: 1, rotation });
|
|
||||||
const avail = Math.max(260, (maximized ? window.innerWidth : (canvasWrap.clientWidth || container.clientWidth || 360)) - 24);
|
|
||||||
scale = maximized ? avail / vw1.width : Math.min(1.5, avail / vw1.width);
|
|
||||||
await renderPage(currentPage);
|
|
||||||
} catch (_) {}
|
|
||||||
};
|
|
||||||
controls.querySelector('.pdf-metadata').onclick = async () => {
|
|
||||||
const info = await pdf.getMetadata();
|
|
||||||
alert(`Title: ${info.info.Title || 'N/A'}\nAuthor: ${info.info.Author || 'N/A'}\nPDF Producer: ${info.info.Producer || 'N/A'}`);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initPdfViewers);
|
|
||||||
} else {
|
|
||||||
initPdfViewers();
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -25,6 +25,13 @@ const writeStateFile = (file, data) => {
|
||||||
};
|
};
|
||||||
let configCache = { at: 0, cfg: null };
|
let configCache = { at: 0, cfg: null };
|
||||||
const CONFIG_TTL_MS = 2000;
|
const CONFIG_TTL_MS = 2000;
|
||||||
|
const CONNECTIONS_MIN = 5;
|
||||||
|
const CONNECTIONS_MAX = 30;
|
||||||
|
const CONNECTIONS_DEFAULT = 10;
|
||||||
|
const connectionsOf = (value) => {
|
||||||
|
const n = Math.round(Number(value));
|
||||||
|
return value === undefined || value === null || value === '' || !Number.isFinite(n) ? CONNECTIONS_DEFAULT : Math.min(CONNECTIONS_MAX, Math.max(CONNECTIONS_MIN, n));
|
||||||
|
};
|
||||||
const writeConfigFile = (data) => { configCache = { at: 0, cfg: null }; return writeStateFile(configFilePath, data); };
|
const writeConfigFile = (data) => { configCache = { at: 0, cfg: null }; return writeStateFile(configFilePath, data); };
|
||||||
const readJsonFile = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } };
|
const readJsonFile = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } };
|
||||||
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
||||||
|
|
@ -160,6 +167,7 @@ const readConfig = () => {
|
||||||
const configData = fs.readFileSync(configFilePath);
|
const configData = fs.readFileSync(configFilePath);
|
||||||
const cfg = JSON.parse(configData);
|
const cfg = JSON.parse(configData);
|
||||||
if (!['whole', 'mutuals', 'only-lan', 'local'].includes(cfg.wish)) cfg.wish = 'whole';
|
if (!['whole', 'mutuals', 'only-lan', 'local'].includes(cfg.wish)) cfg.wish = 'whole';
|
||||||
|
cfg.connections = connectionsOf(cfg.connections);
|
||||||
if (!['whole', 'mutuals', 'following'].includes(cfg.pmVisibility)) cfg.pmVisibility = 'whole';
|
if (!['whole', 'mutuals', 'following'].includes(cfg.pmVisibility)) cfg.pmVisibility = 'whole';
|
||||||
if (!['whole', 'mutuals', 'following'].includes(cfg.mentionsVisibility)) cfg.mentionsVisibility = 'whole';
|
if (!['whole', 'mutuals', 'following'].includes(cfg.mentionsVisibility)) cfg.mentionsVisibility = 'whole';
|
||||||
if (!cfg.phone || typeof cfg.phone !== 'object') cfg.phone = {};
|
if (!cfg.phone || typeof cfg.phone !== 'object') cfg.phone = {};
|
||||||
|
|
@ -207,4 +215,8 @@ module.exports = {
|
||||||
saveServerConfig,
|
saveServerConfig,
|
||||||
readInviteCodes,
|
readInviteCodes,
|
||||||
defaultInvite,
|
defaultInvite,
|
||||||
|
CONNECTIONS_MIN,
|
||||||
|
CONNECTIONS_MAX,
|
||||||
|
CONNECTIONS_DEFAULT,
|
||||||
|
connectionsOf,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ const HIDDEN_ENVELOPE_TYPES = new Set([
|
||||||
'courts-key'
|
'courts-key'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, larpModel }) => {
|
module.exports = ({ cooler, cryptoModel, tribesModel, padsModel, industryModel, larpModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb };
|
||||||
|
|
||||||
|
|
@ -121,26 +121,25 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryDecryptPublicInviteKey = (invites) => {
|
const tryDecryptPublicInviteKey = (invites) => {
|
||||||
if (!tribeCrypto || !Array.isArray(invites)) return null;
|
if (!cryptoModel || !Array.isArray(invites)) return null;
|
||||||
for (const inv of invites) {
|
for (const inv of invites) {
|
||||||
if (!inv || typeof inv !== 'object' || inv.public !== true) continue;
|
if (!inv || typeof inv !== 'object' || inv.public !== true) continue;
|
||||||
if (typeof inv.code !== 'string') continue;
|
if (typeof inv.code !== 'string') continue;
|
||||||
if (typeof inv.ek === 'string') {
|
if (typeof inv.ek === 'string') {
|
||||||
try {
|
try {
|
||||||
const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt);
|
const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt);
|
||||||
if (k) return k;
|
if (k) return k;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (typeof inv.ekChain === 'string') {
|
if (typeof inv.ekChain === 'string') {
|
||||||
try {
|
try {
|
||||||
const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt);
|
const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt);
|
||||||
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key;
|
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const hasBlob = async (ssbClient, url) => new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has)));
|
|
||||||
const getMsg = async (ssbClient, key) => new Promise(resolve => ssbClient.get(key, (err, msg) => resolve(err ? null : msg)));
|
const getMsg = async (ssbClient, key) => new Promise(resolve => ssbClient.get(key, (err, msg) => resolve(err ? null : msg)));
|
||||||
const normNL = (s) => String(s || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
const normNL = (s) => String(s || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
const stripHtml = (s) => normNL(s)
|
const stripHtml = (s) => normNL(s)
|
||||||
|
|
@ -302,7 +301,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1;
|
for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null;
|
||||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||||
let larpWall = null;
|
let larpWall = null;
|
||||||
if (larpModel && results.some(m => m && m.value && m.value.content && m.value.content.type === 'larpHousePost')) {
|
if (larpModel && results.some(m => m && m.value && m.value.content && m.value.content.type === 'larpHousePost')) {
|
||||||
|
|
@ -330,8 +329,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
if (c.type === 'larpHousePost' && !larpPostVisible(v.author, String(c.house || '').toLowerCase())) continue;
|
if (c.type === 'larpHousePost' && !larpPostVisible(v.author, String(c.house || '').toLowerCase())) continue;
|
||||||
if (typeof c.type === 'string' && /e2ee/i.test(c.type)) continue;
|
if (typeof c.type === 'string' && /e2ee/i.test(c.type)) continue;
|
||||||
if (c.type === 'contact') continue;
|
if (c.type === 'contact') continue;
|
||||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel && cryptoModel.isSealedMsg(c)) {
|
||||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null;
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const inner = r.body;
|
const inner = r.body;
|
||||||
if (inner.k !== 'tribe' || inner.op !== 'create') continue;
|
if (inner.k !== 'tribe' || inner.op !== 'create') continue;
|
||||||
|
|
@ -341,11 +340,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
}
|
}
|
||||||
if (!c.type) continue;
|
if (!c.type) continue;
|
||||||
if (c.type === 'tombstone') continue;
|
if (c.type === 'tombstone') continue;
|
||||||
if (c.encryptedPayload && tribeCrypto && !c.tribeId) {
|
if (c.encryptedPayload && cryptoModel && !c.tribeId) {
|
||||||
const pubKey = tryDecryptPublicInviteKey(c.invites);
|
const pubKey = tryDecryptPublicInviteKey(c.invites);
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
try {
|
try {
|
||||||
const dec = tribeCrypto.decryptContent(c, [[pubKey]]);
|
const dec = cryptoModel.decryptContent(c, [[pubKey]]);
|
||||||
if (dec && !dec._undecryptable) c = dec;
|
if (dec && !dec._undecryptable) c = dec;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -618,6 +617,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
feedOpinionsByRoot.get(r).push({ author: v.author, category: String(c.category || '') });
|
feedOpinionsByRoot.get(r).push({ author: v.author, category: String(c.category || '') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reportConfirmsByRoot = new Map();
|
||||||
|
for (const msg of results) {
|
||||||
|
const c = msg && msg.value && msg.value.content;
|
||||||
|
if (!c || c.type !== 'reportConfirm' || typeof c.target !== 'string') continue;
|
||||||
|
const r = rootOf(c.target);
|
||||||
|
if (!reportConfirmsByRoot.has(r)) reportConfirmsByRoot.set(r, new Set());
|
||||||
|
reportConfirmsByRoot.get(r).add(msg.value.author);
|
||||||
|
}
|
||||||
|
|
||||||
const forgedFeedAggByRoot = new Map();
|
const forgedFeedAggByRoot = new Map();
|
||||||
for (const [child, fc] of forgedFeedContent.entries()) {
|
for (const [child, fc] of forgedFeedContent.entries()) {
|
||||||
let cur = child;
|
let cur = child;
|
||||||
|
|
@ -632,8 +640,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
const latest = [];
|
const latest = [];
|
||||||
const docUrls = [...new Set([...idToAction.values()].filter(a => a.type === 'document').map(a => (a.content || {}).url))];
|
|
||||||
const docBlobOk = new Map(await Promise.all(docUrls.map(async url => [url, await hasBlob(ssbClient, url)])));
|
|
||||||
for (const a of idToAction.values()) {
|
for (const a of idToAction.values()) {
|
||||||
if (tombstoned.has(a.id)) continue;
|
if (tombstoned.has(a.id)) continue;
|
||||||
if (a.type === 'tribe' && parentOf.has(a.id)) continue;
|
if (a.type === 'tribe' && parentOf.has(a.id)) continue;
|
||||||
|
|
@ -644,7 +650,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
if (c.key && tombstoned.has(c.key)) continue;
|
if (c.key && tombstoned.has(c.key)) continue;
|
||||||
if (c.branch && tombstoned.has(c.branch)) continue;
|
if (c.branch && tombstoned.has(c.branch)) continue;
|
||||||
if (c.target && tombstoned.has(c.target)) continue;
|
if (c.target && tombstoned.has(c.target)) continue;
|
||||||
if (a.type === 'document' && !docBlobOk.get(c.url)) continue;
|
|
||||||
if (a.type === 'forum' && c.root) {
|
if (a.type === 'forum' && c.root) {
|
||||||
const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || '');
|
const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || '');
|
||||||
const rootAction = idToAction.get(rootId);
|
const rootAction = idToAction.get(rootId);
|
||||||
|
|
@ -696,6 +701,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
}
|
}
|
||||||
content = { ...content, opinions, opinions_inhabitants: voters };
|
content = { ...content, opinions, opinions_inhabitants: voters };
|
||||||
}
|
}
|
||||||
|
if (a.type === 'report' && reportConfirmsByRoot.has(actionRoot)) {
|
||||||
|
content = { ...content, confirmations: [...new Set([...(Array.isArray(content.confirmations) ? content.confirmations : []), ...reportConfirmsByRoot.get(actionRoot)])] };
|
||||||
|
}
|
||||||
latest.push({ ...a, content, ...extra, tipId: idToTipId.get(a.id) || a.id, rootId: actionRoot !== a.id ? actionRoot : null });
|
latest.push({ ...a, content, ...extra, tipId: idToTipId.get(a.id) || a.id, rootId: actionRoot !== a.id ? actionRoot : null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -745,7 +753,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel,
|
||||||
}
|
}
|
||||||
} else if (textTypes.has(a.type)) {
|
} else if (textTypes.has(a.type)) {
|
||||||
const t = norm(c.text);
|
const t = norm(c.text);
|
||||||
const key = t ? `${a.type}:${a.author}:${t}` : null;
|
const key = t ? `${a.type}:${a.author}:${typeof c.root === 'string' ? c.root : ''}:${t}` : null;
|
||||||
const prev = key ? byKey.get(key) : null;
|
const prev = key ? byKey.get(key) : null;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
byKey.set(`id:${a.id}`, { ...a, __effTs: effTs });
|
byKey.set(`id:${a.id}`, { ...a, __effTs: effTs });
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { dedupeBy, norm, versionGroups } = require('../backend/dedupe');
|
const { dedupeBy, norm, versionGroups } = require('../backend/dedupe');
|
||||||
const { readTyped, memoIndex } = require('./typed_log');
|
const { readTyped, memoIndex } = require('./typed_log');
|
||||||
|
|
@ -230,7 +231,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (!oldMsg || oldMsg.content?.type !== "audio") throw new Error("Audio not found");
|
if (!oldMsg || oldMsg.content?.type !== "audio") throw new Error("Audio not found");
|
||||||
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
||||||
const aggAudio = await this.getAudioById(id, userId);
|
const aggAudio = await this.getAudioById(id, userId);
|
||||||
if (aggAudio && Object.keys(aggAudio.opinions || {}).some(k => (aggAudio.opinions[k] || 0) > 0)) throw new Error("Cannot edit audio after it has received opinions.");
|
opinionRules.assertMediaKept(aggAudio, !!blobMarkdown);
|
||||||
|
|
||||||
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
||||||
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const { readContentWindow, requestScope, logTip } = require('./typed_log');
|
||||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
|
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
|
|
@ -41,9 +41,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
return set;
|
return set;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasBlob = async (ssbClient, url) =>
|
|
||||||
new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has)));
|
|
||||||
|
|
||||||
const isReplacedBlock = (vg, tombstoned, block) => {
|
const isReplacedBlock = (vg, tombstoned, block) => {
|
||||||
const c = block.content;
|
const c = block.content;
|
||||||
const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root);
|
const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root);
|
||||||
|
|
@ -142,7 +139,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const idToBlock = new Map();
|
const idToBlock = new Map();
|
||||||
const versions = [];
|
const versions = [];
|
||||||
|
|
||||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null;
|
||||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||||
for (const msg of results) {
|
for (const msg of results) {
|
||||||
const k = msg.key;
|
const k = msg.key;
|
||||||
|
|
@ -157,8 +154,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
if (!c?.type) continue;
|
if (!c?.type) continue;
|
||||||
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
||||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel && cryptoModel.isSealedMsg(c)) {
|
||||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null;
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const inner = r.body;
|
const inner = r.body;
|
||||||
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
||||||
|
|
@ -264,7 +261,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const tombstoned = buildValidatedTombstoneSet(results);
|
const tombstoned = buildValidatedTombstoneSet(results);
|
||||||
const idToBlock = new Map();
|
const idToBlock = new Map();
|
||||||
const versions = [];
|
const versions = [];
|
||||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null;
|
||||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||||
|
|
||||||
for (const msg of results) {
|
for (const msg of results) {
|
||||||
|
|
@ -280,8 +277,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
if (!c?.type) continue;
|
if (!c?.type) continue;
|
||||||
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
||||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel && cryptoModel.isSealedMsg(c)) {
|
||||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null;
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const inner = r.body;
|
const inner = r.body;
|
||||||
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
||||||
|
|
@ -300,11 +297,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const block = idToBlock.get(id);
|
const block = idToBlock.get(id);
|
||||||
if (!block) return null;
|
if (!block) return null;
|
||||||
|
|
||||||
if (block.type === 'document') {
|
|
||||||
const valid = await hasBlob(ssbClient, block.content.url);
|
|
||||||
if (!valid) return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTombstoned = tombstoned.has(block.id);
|
const isTombstoned = tombstoned.has(block.id);
|
||||||
const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block);
|
const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const { getConfig } = require('../configs/config-manager.js');
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { readTyped, memoIndex } = require('./typed_log');
|
const { readTyped, memoIndex } = require('./typed_log');
|
||||||
const longText = require('../backend/long_text');
|
const longText = require('../backend/long_text');
|
||||||
|
const { isComment, isBlogPost } = require('./comments_model');
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
||||||
const OPINION_TYPE = 'blogOpinion';
|
const OPINION_TYPE = 'blogOpinion';
|
||||||
|
|
@ -15,9 +16,6 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
|
|
||||||
const getAllMessages = async (ssbClient) => readTyped(ssbClient, BLOG_TYPES, { limit: logLimit });
|
const getAllMessages = async (ssbClient) => readTyped(ssbClient, BLOG_TYPES, { limit: logLimit });
|
||||||
|
|
||||||
const isRootPost = (c) =>
|
|
||||||
!!c && c.type === 'post' && typeof c.text === 'string' &&
|
|
||||||
!c.root && !c.fork && !c.branch && !c.about && !c.target;
|
|
||||||
|
|
||||||
const buildIndex = (messages) => {
|
const buildIndex = (messages) => {
|
||||||
const tomb = buildValidatedTombstoneSet(messages);
|
const tomb = buildValidatedTombstoneSet(messages);
|
||||||
|
|
@ -33,9 +31,9 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
if (tomb.has(m.key)) continue;
|
if (tomb.has(m.key)) continue;
|
||||||
|
|
||||||
if (c.type === 'post' && typeof c.text === 'string') {
|
if (c.type === 'post' && typeof c.text === 'string') {
|
||||||
if (isRootPost(c)) {
|
if (isBlogPost(c)) {
|
||||||
posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c });
|
posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c });
|
||||||
} else if (typeof c.root === 'string') {
|
} else if (isComment(c)) {
|
||||||
commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1);
|
commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -142,7 +140,7 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const userId = ssbClient.id;
|
const userId = ssbClient.id;
|
||||||
const msg = await new Promise((res, rej) => ssbClient.get(id, (err, m) => (err || !m || !m.content) ? rej(new Error('Blog not found')) : res(m)));
|
const msg = await new Promise((res, rej) => ssbClient.get(id, (err, m) => (err || !m || !m.content) ? rej(new Error('Blog not found')) : res(m)));
|
||||||
if (!isRootPost(msg.content)) throw new Error('Blog not found');
|
if (!isBlogPost(msg.content)) throw new Error('Blog not found');
|
||||||
if (msg.author !== userId) throw new Error('Not the author');
|
if (msg.author !== userId) throw new Error('Not the author');
|
||||||
const content = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
|
const content = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
|
||||||
return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)));
|
return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)));
|
||||||
|
|
@ -157,27 +155,6 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined');
|
if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined');
|
||||||
const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() };
|
const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() };
|
||||||
return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)));
|
return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)));
|
||||||
},
|
|
||||||
|
|
||||||
async resolveRootId(id) {
|
|
||||||
const ssbClient = await openSsb();
|
|
||||||
const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m)));
|
|
||||||
const c = msg && (msg.content || (msg.value && msg.value.content));
|
|
||||||
if (c && c.type === 'post' && typeof c.root === 'string') return c.root;
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
|
|
||||||
async blogHrefFor(id) {
|
|
||||||
const ssbClient = await openSsb();
|
|
||||||
const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m)));
|
|
||||||
const c = msg && (msg.content || (msg.value && msg.value.content));
|
|
||||||
if (!c || c.type !== 'post' || typeof c.text !== 'string') return null;
|
|
||||||
if (c.private === true || Array.isArray(c.recps)) return null;
|
|
||||||
if (typeof c.root === 'string') {
|
|
||||||
return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`;
|
|
||||||
}
|
|
||||||
if (c.fork || c.branch || c.about || c.target) return null;
|
|
||||||
return `/blogs/${encodeURIComponent(id)}`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const categories = require("../backend/opinion_categories");
|
const categories = require("../backend/opinion_categories");
|
||||||
|
|
@ -29,6 +30,8 @@ const coerceLastVisit = (lastVisit) => {
|
||||||
const voteSum = (opinions = {}) =>
|
const voteSum = (opinions = {}) =>
|
||||||
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||||
|
|
||||||
|
const BOOKMARK_TITLE_MAX = 100;
|
||||||
|
|
||||||
module.exports = ({ cooler }) => {
|
module.exports = ({ cooler }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
|
|
||||||
|
|
@ -103,6 +106,7 @@ module.exports = ({ cooler }) => {
|
||||||
id: node.key,
|
id: node.key,
|
||||||
rootId,
|
rootId,
|
||||||
url: c.url || "",
|
url: c.url || "",
|
||||||
|
title: c.title || "",
|
||||||
createdAt: c.createdAt || new Date(node.ts).toISOString(),
|
createdAt: c.createdAt || new Date(node.ts).toISOString(),
|
||||||
updatedAt: c.updatedAt || null,
|
updatedAt: c.updatedAt || null,
|
||||||
lastVisit: c.lastVisit || null,
|
lastVisit: c.lastVisit || null,
|
||||||
|
|
@ -139,7 +143,7 @@ module.exports = ({ cooler }) => {
|
||||||
return root;
|
return root;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createBookmark(url, tagsRaw, description, lastVisit) {
|
async createBookmark(url, tagsRaw, description, lastVisit, title = "") {
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
|
@ -150,6 +154,7 @@ module.exports = ({ cooler }) => {
|
||||||
type: "bookmark",
|
type: "bookmark",
|
||||||
author: ssbClient.id,
|
author: ssbClient.id,
|
||||||
url: u,
|
url: u,
|
||||||
|
...(safeText(title) ? { title: safeText(title).slice(0, BOOKMARK_TITLE_MAX) } : {}),
|
||||||
tags: normalizeTags(tagsRaw),
|
tags: normalizeTags(tagsRaw),
|
||||||
description: description || "",
|
description: description || "",
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -174,7 +179,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (!oldMsg || oldMsg.content?.type !== "bookmark") throw new Error("Bookmark not found");
|
if (!oldMsg || oldMsg.content?.type !== "bookmark") throw new Error("Bookmark not found");
|
||||||
if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author");
|
if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author");
|
||||||
const aggBm = await this.getBookmarkById(id, userId);
|
const aggBm = await this.getBookmarkById(id, userId);
|
||||||
if (aggBm && Object.keys(aggBm.opinions || {}).some(k => (aggBm.opinions[k] || 0) > 0)) throw new Error("Cannot edit bookmark after it has received opinions.");
|
opinionRules.assertMediaKept(aggBm, updatedData.url !== undefined && safeText(updatedData.url) !== safeText(oldMsg.content.url));
|
||||||
|
|
||||||
const url = safeText(updatedData.url || oldMsg.content.url);
|
const url = safeText(updatedData.url || oldMsg.content.url);
|
||||||
if (!url) throw new Error("URL is required");
|
if (!url) throw new Error("URL is required");
|
||||||
|
|
@ -185,6 +190,7 @@ module.exports = ({ cooler }) => {
|
||||||
...oldMsg.content,
|
...oldMsg.content,
|
||||||
replaces: tipId,
|
replaces: tipId,
|
||||||
url,
|
url,
|
||||||
|
title: updatedData.title !== undefined ? safeText(updatedData.title).slice(0, BOOKMARK_TITLE_MAX) : (oldMsg.content.title || ""),
|
||||||
tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags),
|
tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags),
|
||||||
description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "",
|
description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "",
|
||||||
lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit),
|
lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit),
|
||||||
|
|
|
||||||
|
|
@ -51,31 +51,31 @@ const assertCalendarDates = ({ deadline, date, until }) => {
|
||||||
if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline")
|
if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline")
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => {
|
module.exports = ({ cooler, pmModel, cryptoModel, calendarCrypto, tribesModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
const ownCrypto = calendarCrypto || tribeCrypto
|
const ownCrypto = calendarCrypto || cryptoModel
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null
|
||||||
const lookupKeys = (rid) => {
|
const lookupKeys = (rid) => {
|
||||||
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
||||||
if (a.length) return a
|
if (a.length) return a
|
||||||
return (tribeCrypto && tribeCrypto.getKeys(rid)) || []
|
return (cryptoModel && cryptoModel.getKeys(rid)) || []
|
||||||
}
|
}
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0)
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0)
|
||||||
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } }
|
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } }
|
||||||
|
|
||||||
const rotateCalendarKey = async (rootId, remainingMembers) => {
|
const rotateCalendarKey = async (rootId, remainingMembers) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rootId) return
|
if (!ownCrypto || !cryptoModel || !rootId) return
|
||||||
const existing = lookupKey(rootId)
|
const existing = lookupKey(rootId)
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
const newKey = ownCrypto.generateTribeKey()
|
const newKey = ownCrypto.generateKey()
|
||||||
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
||||||
const ssbClient = await openSsb()
|
const ssbClient = await openSsb()
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -115,7 +115,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
const readAll = async (ssbClient) => unwrapForIndex(await readTyped(ssbClient, CALENDAR_TYPES, { limit: logLimit, withWindow: true }))
|
const readAll = async (ssbClient) => unwrapForIndex(await readTyped(ssbClient, CALENDAR_TYPES, { limit: logLimit, withWindow: true }))
|
||||||
|
|
||||||
const ensureMemberKeys = async (ssbClient, messages, cals) => {
|
const ensureMemberKeys = async (ssbClient, messages, cals) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return
|
if (!cryptoModel || !ownCrypto) return
|
||||||
const leavesByRoot = new Map()
|
const leavesByRoot = new Map()
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content
|
const c = m.value && m.value.content
|
||||||
|
|
@ -138,7 +138,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
const key = lookupKey(rootId)
|
const key = lookupKey(rootId)
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue
|
if (!Object.keys(memberKeys).length) continue
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -147,50 +147,50 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null
|
const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null
|
||||||
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, CALENDAR_TYPES) : msgs
|
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, CALENDAR_TYPES) : msgs
|
||||||
const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c
|
const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c
|
||||||
const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c
|
const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c
|
||||||
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}
|
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}
|
||||||
|
|
||||||
const encryptStandalone = (content, rootId) => {
|
const encryptStandalone = (content, rootId) => {
|
||||||
if (!tribeCrypto || !rootId) return content
|
if (!cryptoModel || !rootId) return content
|
||||||
const key = lookupKey(rootId)
|
const key = lookupKey(rootId)
|
||||||
if (!key) return content
|
if (!key) return content
|
||||||
return tribeCrypto.encryptContent(content, [key], true)
|
return cryptoModel.encryptContent(content, [key], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptScoped = async (content, rootId) => {
|
const decryptScoped = async (content, rootId) => {
|
||||||
if (!content) return content
|
if (!content) return content
|
||||||
if (content.tribeId || (tribeCrypto && tribeCrypto.isTribeMsg(content))) return await decryptIfTribe(content)
|
if (content.tribeId || (cryptoModel && cryptoModel.isSealedMsg(content))) return await decryptIfGroup(content)
|
||||||
return decryptCalendarRoot(content, rootId)
|
return decryptCalendarRoot(content, rootId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptCalendarRoot = (content, rootId) => {
|
const decryptCalendarRoot = (content, rootId) => {
|
||||||
if (!content || !content.encryptedPayload) return content
|
if (!content || !content.encryptedPayload) return content
|
||||||
if (!tribeCrypto) return content
|
if (!cryptoModel) return content
|
||||||
const keys = lookupKeys(rootId)
|
const keys = lookupKeys(rootId)
|
||||||
if (!keys || !keys.length) return { ...content, _undecryptable: true }
|
if (!keys || !keys.length) return { ...content, _undecryptable: true }
|
||||||
return tribeCrypto.decryptContent(content, keys.map(k => [k]))
|
return cryptoModel.decryptContent(content, keys.map(k => [k]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tryDecryptPublicInviteKey = (invites) => {
|
const tryDecryptPublicInviteKey = (invites) => {
|
||||||
if (!tribeCrypto || !Array.isArray(invites)) return null
|
if (!cryptoModel || !Array.isArray(invites)) return null
|
||||||
for (const inv of invites) {
|
for (const inv of invites) {
|
||||||
if (!inv || typeof inv !== "object" || inv.public !== true) continue
|
if (!inv || typeof inv !== "object" || inv.public !== true) continue
|
||||||
if (typeof inv.code !== "string") continue
|
if (typeof inv.code !== "string") continue
|
||||||
if (typeof inv.ek === "string") {
|
if (typeof inv.ek === "string") {
|
||||||
try { const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k } catch (_) {}
|
try { const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k } catch (_) {}
|
||||||
}
|
}
|
||||||
if (typeof inv.ekChain === "string") {
|
if (typeof inv.ekChain === "string") {
|
||||||
try { const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key } catch (_) {}
|
try { const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptIndexNodes = async (idx) => {
|
const decryptIndexNodes = async (idx) => {
|
||||||
if (!tribeCrypto) return
|
if (!cryptoModel) return
|
||||||
for (const [k, n] of idx.nodes.entries()) {
|
for (const [k, n] of idx.nodes.entries()) {
|
||||||
if (!n.c || !n.c.encryptedPayload) continue
|
if (!n.c || !n.c.encryptedPayload) continue
|
||||||
let root = k
|
let root = k
|
||||||
|
|
@ -198,7 +198,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
let dec = null
|
let dec = null
|
||||||
if (n.c.tribeId && tribesModel) {
|
if (n.c.tribeId && tribesModel) {
|
||||||
try {
|
try {
|
||||||
const r = await tribeCrypto.decryptFromTribe(n.c, tribesModel)
|
const r = await cryptoModel.decryptFromGroup(n.c, tribesModel)
|
||||||
if (r && !r._undecryptable) dec = r
|
if (r && !r._undecryptable) dec = r
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +209,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (!dec) {
|
if (!dec) {
|
||||||
const pubKey = tryDecryptPublicInviteKey(n.c.invites)
|
const pubKey = tryDecryptPublicInviteKey(n.c.invites)
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
try { const r = tribeCrypto.decryptContent(n.c, [[pubKey]]); if (r && !r._undecryptable) dec = r } catch (_) {}
|
try { const r = cryptoModel.decryptContent(n.c, [[pubKey]]); if (r && !r._undecryptable) dec = r } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dec) {
|
if (dec) {
|
||||||
|
|
@ -438,10 +438,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
let calKey = null
|
let calKey = null
|
||||||
let content = plainContent
|
let content = plainContent
|
||||||
if (tribeId) {
|
if (tribeId) {
|
||||||
content = await encryptIfTribe(plainContent)
|
content = await encryptIfGroup(plainContent)
|
||||||
} else if (tribeCrypto) {
|
} else if (cryptoModel) {
|
||||||
calKey = ownCrypto.generateTribeKey()
|
calKey = ownCrypto.generateKey()
|
||||||
content = tribeCrypto.encryptContent(plainContent, [calKey], true)
|
content = cryptoModel.encryptContent(plainContent, [calKey], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const calMsg = await new Promise((resolve, reject) => {
|
const calMsg = await new Promise((resolve, reject) => {
|
||||||
|
|
@ -450,11 +450,11 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
|
|
||||||
const calendarId = calMsg.key
|
const calendarId = calMsg.key
|
||||||
|
|
||||||
if (calKey && tribeCrypto) {
|
if (calKey && cryptoModel) {
|
||||||
ownCrypto.setKey(calendarId, calKey, 1)
|
ownCrypto.setKey(calendarId, calKey, 1)
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const boxedKey = tribeCrypto.boxKeyForMember(calKey, userId, ssbKeys)
|
const boxedKey = cryptoModel.boxKeyForMember(calKey, userId, ssbKeys)
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: calendarId, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve())
|
ssbClient.publish({ type: "tribe-keys", tribeId: calendarId, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve())
|
||||||
})
|
})
|
||||||
|
|
@ -462,8 +462,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (validStatus === "OPEN") {
|
if (validStatus === "OPEN") {
|
||||||
try {
|
try {
|
||||||
const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex")
|
const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex")
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt()
|
const inviteSalt = cryptoModel.generateInviteSalt()
|
||||||
const ek = tribeCrypto.encryptForInvite(calKey, pubCode, inviteSalt)
|
const ek = cryptoModel.encryptForInvite(calKey, pubCode, inviteSalt)
|
||||||
const tipId = await this.resolveCurrentId(calendarId)
|
const tipId = await this.resolveCurrentId(calendarId)
|
||||||
const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it)))
|
const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it)))
|
||||||
const dec = decryptCalendarRoot(item.content, calendarId)
|
const dec = decryptCalendarRoot(item.content, calendarId)
|
||||||
|
|
@ -501,8 +501,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}),
|
...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}),
|
||||||
...(tribeId ? { tribeId } : {})
|
...(tribeId ? { tribeId } : {})
|
||||||
}
|
}
|
||||||
if (tribeId) dateContent = await encryptIfTribe(dateContent)
|
if (tribeId) dateContent = await encryptIfGroup(dateContent)
|
||||||
else if (calKey) dateContent = tribeCrypto.encryptContent(dateContent, [calKey], true)
|
else if (calKey) dateContent = cryptoModel.encryptContent(dateContent, [calKey], true)
|
||||||
const dateMsg = await new Promise((resolve, reject) => {
|
const dateMsg = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(dateContent, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(dateContent, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
})
|
})
|
||||||
|
|
@ -517,8 +517,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
...(tribeId ? { tribeId } : {})
|
...(tribeId ? { tribeId } : {})
|
||||||
}
|
}
|
||||||
if (tribeId) noteContent = await encryptIfTribe(noteContent)
|
if (tribeId) noteContent = await encryptIfGroup(noteContent)
|
||||||
else if (calKey) noteContent = tribeCrypto.encryptContent(noteContent, [calKey], true)
|
else if (calKey) noteContent = cryptoModel.encryptContent(noteContent, [calKey], true)
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
})
|
})
|
||||||
|
|
@ -561,7 +561,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
updated.invites = updated.invites.filter(inv => !(inv && typeof inv === "object" && inv.public === true))
|
updated.invites = updated.invites.filter(inv => !(inv && typeof inv === "object" && inv.public === true))
|
||||||
try { await rotateCalendarKey(rootId, (updated.participants || []).filter(p => p !== userId)) } catch (_) {}
|
try { await rotateCalendarKey(rootId, (updated.participants || []).filter(p => p !== userId)) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (updated.tribeId) updated = await encryptIfTribe(updated)
|
if (updated.tribeId) updated = await encryptIfGroup(updated)
|
||||||
else updated = encryptStandalone(updated, rootId)
|
else updated = encryptStandalone(updated, rootId)
|
||||||
const result = await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
const result = await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
||||||
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
||||||
|
|
@ -591,7 +591,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (!cal) throw new Error("Calendar not found")
|
if (!cal) throw new Error("Calendar not found")
|
||||||
const participants = Array.isArray(cal.participants) ? cal.participants : []
|
const participants = Array.isArray(cal.participants) ? cal.participants : []
|
||||||
if (participants.includes(userId)) return
|
if (participants.includes(userId)) return
|
||||||
if (tribeCrypto && Array.isArray(cal.invites)) {
|
if (cryptoModel && Array.isArray(cal.invites)) {
|
||||||
const pub = cal.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain))
|
const pub = cal.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain))
|
||||||
if (pub) return await this.joinByInvite(pub.code)
|
if (pub) return await this.joinByInvite(pub.code)
|
||||||
}
|
}
|
||||||
|
|
@ -693,7 +693,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
...(ruleDeadline ? { intervalDeadline: ruleDeadline } : {}),
|
...(ruleDeadline ? { intervalDeadline: ruleDeadline } : {}),
|
||||||
...(cal.tribeId ? { tribeId: cal.tribeId } : {})
|
...(cal.tribeId ? { tribeId: cal.tribeId } : {})
|
||||||
}
|
}
|
||||||
if (cal.tribeId) dateContent = await encryptIfTribe(dateContent)
|
if (cal.tribeId) dateContent = await encryptIfGroup(dateContent)
|
||||||
else dateContent = encryptStandalone(dateContent, rootId)
|
else dateContent = encryptStandalone(dateContent, rootId)
|
||||||
const msg = await new Promise((resolve, reject) => {
|
const msg = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(dateContent, (err, m) => err ? reject(err) : resolve(m))
|
ssbClient.publish(dateContent, (err, m) => err ? reject(err) : resolve(m))
|
||||||
|
|
@ -728,15 +728,15 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (c.calendarId !== rootId) continue
|
if (c.calendarId !== rootId) continue
|
||||||
if (!members.has(v.author)) continue
|
if (!members.has(v.author)) continue
|
||||||
let dec = c
|
let dec = c
|
||||||
if (c.encryptedPayload && tribeCrypto) {
|
if (c.encryptedPayload && cryptoModel) {
|
||||||
if (c.tribeId && tribesModel) {
|
if (c.tribeId && tribesModel) {
|
||||||
const r = await tribeCrypto.decryptFromTribe(c, tribesModel)
|
const r = await cryptoModel.decryptFromGroup(c, tribesModel)
|
||||||
dec = r && !r._undecryptable ? r : c
|
dec = r && !r._undecryptable ? r : c
|
||||||
if (r && r._undecryptable) continue
|
if (r && r._undecryptable) continue
|
||||||
} else {
|
} else {
|
||||||
const keys = [...(lookupKeys(c.calendarId) || []), ...(pubKey ? [pubKey] : [])]
|
const keys = [...(lookupKeys(c.calendarId) || []), ...(pubKey ? [pubKey] : [])]
|
||||||
if (!keys.length) continue
|
if (!keys.length) continue
|
||||||
const r = tribeCrypto.decryptContent(c, keys.map(k => [k]))
|
const r = cryptoModel.decryptContent(c, keys.map(k => [k]))
|
||||||
if (!r || r._undecryptable) continue
|
if (!r || r._undecryptable) continue
|
||||||
dec = r
|
dec = r
|
||||||
}
|
}
|
||||||
|
|
@ -789,8 +789,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (!c || c.type !== "calendarDate") continue
|
if (!c || c.type !== "calendarDate") continue
|
||||||
if (tombstoned.has(m.key)) break
|
if (tombstoned.has(m.key)) break
|
||||||
let dec = c
|
let dec = c
|
||||||
if (c.encryptedPayload && tribeCrypto && tribesModel) {
|
if (c.encryptedPayload && cryptoModel && tribesModel) {
|
||||||
const r = await tribeCrypto.decryptFromTribe(c, tribesModel)
|
const r = await cryptoModel.decryptFromGroup(c, tribesModel)
|
||||||
if (r && !r._undecryptable) dec = r
|
if (r && !r._undecryptable) dec = r
|
||||||
}
|
}
|
||||||
dateAuthor = dec.author || (m.value || {}).author
|
dateAuthor = dec.author || (m.value || {}).author
|
||||||
|
|
@ -828,7 +828,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
...(cal.tribeId ? { tribeId: cal.tribeId } : {})
|
...(cal.tribeId ? { tribeId: cal.tribeId } : {})
|
||||||
}
|
}
|
||||||
if (cal.tribeId) noteContent = await encryptIfTribe(noteContent)
|
if (cal.tribeId) noteContent = await encryptIfGroup(noteContent)
|
||||||
else noteContent = encryptStandalone(noteContent, rootId)
|
else noteContent = encryptStandalone(noteContent, rootId)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
|
|
@ -871,12 +871,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
let dec = c
|
let dec = c
|
||||||
if (c.encryptedPayload) {
|
if (c.encryptedPayload) {
|
||||||
let r = await decryptScoped(c, rootId)
|
let r = await decryptScoped(c, rootId)
|
||||||
if ((!r || r._undecryptable) && !c.tribeId && tribeCrypto) {
|
if ((!r || r._undecryptable) && !c.tribeId && cryptoModel) {
|
||||||
if (pubKey === undefined) {
|
if (pubKey === undefined) {
|
||||||
const cal = await this.getCalendarById(rootId).catch(() => null)
|
const cal = await this.getCalendarById(rootId).catch(() => null)
|
||||||
pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null
|
pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null
|
||||||
}
|
}
|
||||||
if (pubKey) r = tribeCrypto.decryptContent(c, [[pubKey]])
|
if (pubKey) r = cryptoModel.decryptContent(c, [[pubKey]])
|
||||||
}
|
}
|
||||||
if (r && !r._undecryptable) dec = r
|
if (r && !r._undecryptable) dec = r
|
||||||
else continue
|
else continue
|
||||||
|
|
@ -924,8 +924,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
const dated = messages.filter(m => !tombstoned.has(m.key) && ((m.value || {}).content || {}).type === "calendarDate")
|
const dated = messages.filter(m => !tombstoned.has(m.key) && ((m.value || {}).content || {}).type === "calendarDate")
|
||||||
const decoded = await Promise.all(dated.map(async (m) => {
|
const decoded = await Promise.all(dated.map(async (m) => {
|
||||||
const c = m.value.content
|
const c = m.value.content
|
||||||
if (!(c.encryptedPayload && tribeCrypto && tribesModel)) return c
|
if (!(c.encryptedPayload && cryptoModel && tribesModel)) return c
|
||||||
const r = await tribeCrypto.decryptFromTribe(c, tribesModel)
|
const r = await cryptoModel.decryptFromGroup(c, tribesModel)
|
||||||
return !r || r._undecryptable ? null : r
|
return !r || r._undecryptable ? null : r
|
||||||
}))
|
}))
|
||||||
for (let i = 0; i < dated.length; i++) {
|
for (let i = 0; i < dated.length; i++) {
|
||||||
|
|
@ -1013,12 +1013,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex")
|
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex")
|
||||||
let invite = code
|
let invite = code
|
||||||
const calKey = !cal.tribeId ? lookupKey(cal.rootId) : null
|
const calKey = !cal.tribeId ? lookupKey(cal.rootId) : null
|
||||||
if (tribeCrypto && calKey) {
|
if (cryptoModel && calKey) {
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt()
|
const inviteSalt = cryptoModel.generateInviteSalt()
|
||||||
const ek = tribeCrypto.encryptForInvite(calKey, code, inviteSalt)
|
const ek = cryptoModel.encryptForInvite(calKey, code, inviteSalt)
|
||||||
invite = opts.public
|
invite = opts.public
|
||||||
? { code, ek, salt: inviteSalt, gen: lookupGen(cal.rootId), public: true }
|
? { code, ek, salt: inviteSalt, gen: lookupGen(cal.rootId), public: true }
|
||||||
: { ch: tribeCrypto.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) }
|
: { ch: cryptoModel.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) }
|
||||||
}
|
}
|
||||||
if (opts.public && typeof invite !== "object") invite = { code, public: true }
|
if (opts.public && typeof invite !== "object") invite = { code, public: true }
|
||||||
const tipId = await this.resolveCurrentId(calendarId)
|
const tipId = await this.resolveCurrentId(calendarId)
|
||||||
|
|
@ -1041,7 +1041,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
replaces: tipId
|
replaces: tipId
|
||||||
}
|
}
|
||||||
if (updated.tribeId) updated = await encryptIfTribe(updated)
|
if (updated.tribeId) updated = await encryptIfGroup(updated)
|
||||||
else updated = encryptStandalone(updated, cal.rootId)
|
else updated = encryptStandalone(updated, cal.rootId)
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
||||||
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
||||||
|
|
@ -1087,7 +1087,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
replaces: tipId
|
replaces: tipId
|
||||||
}
|
}
|
||||||
if (updated.tribeId) updated = await encryptIfTribe(updated)
|
if (updated.tribeId) updated = await encryptIfGroup(updated)
|
||||||
else updated = encryptStandalone(updated, cal.rootId)
|
else updated = encryptStandalone(updated, cal.rootId)
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
||||||
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
||||||
|
|
@ -1105,15 +1105,15 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
const invs = Array.isArray(cal.invites) ? cal.invites : []
|
const invs = Array.isArray(cal.invites) ? cal.invites : []
|
||||||
for (const inv of invs) {
|
for (const inv of invs) {
|
||||||
if (typeof inv === "string" && inv === code) { matched = cal; matchedInvite = inv; break }
|
if (typeof inv === "string" && inv === code) { matched = cal; matchedInvite = inv; break }
|
||||||
if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && tribeCrypto && inv.ch === tribeCrypto.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break }
|
if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && cryptoModel && inv.ch === cryptoModel.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break }
|
||||||
}
|
}
|
||||||
if (matched) break
|
if (matched) break
|
||||||
}
|
}
|
||||||
if (!matched) throw new Error("Invalid or expired invite code")
|
if (!matched) throw new Error("Invalid or expired invite code")
|
||||||
if (matched.participants.includes(userId)) throw new Error("Already a participant")
|
if (matched.participants.includes(userId)) throw new Error("Already a participant")
|
||||||
if (typeof matchedInvite === "object" && matchedInvite.ch && tribeCrypto && matchedInvite.ek) {
|
if (typeof matchedInvite === "object" && matchedInvite.ch && cryptoModel && matchedInvite.ek) {
|
||||||
let probeKey = null
|
let probeKey = null
|
||||||
try { probeKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {}
|
try { probeKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {}
|
||||||
const messages = await readAll(await openSsb())
|
const messages = await readAll(await openSsb())
|
||||||
const spent = messages.some(m => {
|
const spent = messages.some(m => {
|
||||||
const c = m.value && m.value.content
|
const c = m.value && m.value.content
|
||||||
|
|
@ -1122,21 +1122,21 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
if (spent) throw new Error("Invite already used")
|
if (spent) throw new Error("Invite already used")
|
||||||
}
|
}
|
||||||
let calKey = null
|
let calKey = null
|
||||||
if (tribeCrypto && typeof matchedInvite === "object") {
|
if (cryptoModel && typeof matchedInvite === "object") {
|
||||||
if (matchedInvite.ekChain) {
|
if (matchedInvite.ekChain) {
|
||||||
const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3)
|
const chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3)
|
||||||
if (Array.isArray(chain) && chain.length) {
|
if (Array.isArray(chain) && chain.length) {
|
||||||
for (const entry of chain) {
|
for (const entry of chain) {
|
||||||
if (Array.isArray(entry.keys) && entry.keys.length) {
|
if (Array.isArray(entry.keys) && entry.keys.length) {
|
||||||
tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length)
|
cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length)
|
||||||
} else if (entry.key) {
|
} else if (entry.key) {
|
||||||
tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1)
|
cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
calKey = chain[0].key
|
calKey = chain[0].key
|
||||||
}
|
}
|
||||||
} else if (matchedInvite.ek) {
|
} else if (matchedInvite.ek) {
|
||||||
calKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt)
|
calKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt)
|
||||||
ownCrypto.setKey(matched.rootId, calKey, matchedInvite.gen || 1)
|
ownCrypto.setKey(matched.rootId, calKey, matchedInvite.gen || 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1166,18 +1166,18 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
replaces: tipId
|
replaces: tipId
|
||||||
}
|
}
|
||||||
if (updated.tribeId) updated = await encryptIfTribe(updated)
|
if (updated.tribeId) updated = await encryptIfGroup(updated)
|
||||||
else updated = encryptStandalone(updated, matched.rootId)
|
else updated = encryptStandalone(updated, matched.rootId)
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res)))
|
||||||
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve()))
|
await new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve()))
|
||||||
if (tribeCrypto && calKey) {
|
if (cryptoModel && calKey) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(calKey, userId, ssbKeys) } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(calKey, userId, ssbKeys) } catch (_) {}
|
||||||
if (matched.author && matched.author !== userId) {
|
if (matched.author && matched.author !== userId) {
|
||||||
try { memberKeys[matched.author] = tribeCrypto.boxKeyForMember(calKey, matched.author, ssbKeys) } catch (_) {}
|
try { memberKeys[matched.author] = cryptoModel.boxKeyForMember(calKey, matched.author, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const REACTION_EMOJIS = ["up", "heart", "laugh", "down"]
|
||||||
|
|
||||||
const DEFAULT_MESSAGES_PER_HOUR = 60
|
const DEFAULT_MESSAGES_PER_HOUR = 60
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, chatCrypto, tribesModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
|
|
@ -35,9 +35,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const ownCrypto = chatCrypto || tribeCrypto
|
const ownCrypto = chatCrypto || cryptoModel
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null
|
||||||
const hashCode = (code, salt) => tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null
|
const hashCode = (code, salt) => cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null
|
||||||
const inviteMatches = (inv, token) => {
|
const inviteMatches = (inv, token) => {
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
if (typeof inv === "string") return inv === token
|
if (typeof inv === "string") return inv === token
|
||||||
|
|
@ -51,21 +51,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
const lookupKeys = (rid) => {
|
const lookupKeys = (rid) => {
|
||||||
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
||||||
if (a.length) return a
|
if (a.length) return a
|
||||||
return (tribeCrypto && tribeCrypto.getKeys(rid)) || []
|
return (cryptoModel && cryptoModel.getKeys(rid)) || []
|
||||||
}
|
}
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0)
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0)
|
||||||
|
|
||||||
const rotateChatKey = async (rootId, remainingMembers) => {
|
const rotateChatKey = async (rootId, remainingMembers) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rootId) return
|
if (!ownCrypto || !cryptoModel || !rootId) return
|
||||||
const existing = lookupKey(rootId)
|
const existing = lookupKey(rootId)
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
const newKey = ownCrypto.generateTribeKey()
|
const newKey = ownCrypto.generateKey()
|
||||||
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
||||||
const ssbClient = await openSsb()
|
const ssbClient = await openSsb()
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -104,10 +104,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTribeKeysFor = async (tribeId) => {
|
const getTribeKeysFor = async (tribeId) => {
|
||||||
if (!tribeCrypto || !tribesModel || !tribeId) return []
|
if (!cryptoModel || !tribesModel || !tribeId) return []
|
||||||
try {
|
try {
|
||||||
const rootId = await tribesModel.getRootId(tribeId)
|
const rootId = await tribesModel.getRootId(tribeId)
|
||||||
return tribeCrypto.getKeys(rootId) || []
|
return cryptoModel.getKeys(rootId) || []
|
||||||
} catch (_) { return [] }
|
} catch (_) { return [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,25 +258,25 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveKeyChainSets = (chatRootId) => {
|
const resolveKeyChainSets = (chatRootId) => {
|
||||||
if (!tribeCrypto) return []
|
if (!cryptoModel) return []
|
||||||
const keys = lookupKeys(chatRootId)
|
const keys = lookupKeys(chatRootId)
|
||||||
return keys.map(k => [k])
|
return keys.map(k => [k])
|
||||||
}
|
}
|
||||||
|
|
||||||
const tryDecryptPublicInviteKey = (invites) => {
|
const tryDecryptPublicInviteKey = (invites) => {
|
||||||
if (!tribeCrypto || !Array.isArray(invites)) return null
|
if (!cryptoModel || !Array.isArray(invites)) return null
|
||||||
for (const inv of invites) {
|
for (const inv of invites) {
|
||||||
if (!inv || typeof inv !== "object" || inv.public !== true) continue
|
if (!inv || typeof inv !== "object" || inv.public !== true) continue
|
||||||
if (typeof inv.code !== "string") continue
|
if (typeof inv.code !== "string") continue
|
||||||
if (typeof inv.ek === "string") {
|
if (typeof inv.ek === "string") {
|
||||||
try {
|
try {
|
||||||
const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt)
|
const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt)
|
||||||
if (k) return k
|
if (k) return k
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (typeof inv.ekChain === "string") {
|
if (typeof inv.ekChain === "string") {
|
||||||
try {
|
try {
|
||||||
const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt)
|
const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt)
|
||||||
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key
|
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -287,14 +287,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
const decryptChatContent = (rawC, rootId) => {
|
const decryptChatContent = (rawC, rootId) => {
|
||||||
let c = rawC
|
let c = rawC
|
||||||
let undecryptable = false
|
let undecryptable = false
|
||||||
if (tribeCrypto && c.encryptedPayload) {
|
if (cryptoModel && c.encryptedPayload) {
|
||||||
const keyChainSets = resolveKeyChainSets(rootId)
|
const keyChainSets = resolveKeyChainSets(rootId)
|
||||||
c = tribeCrypto.decryptContent(c, keyChainSets)
|
c = cryptoModel.decryptContent(c, keyChainSets)
|
||||||
undecryptable = !!c._undecryptable
|
undecryptable = !!c._undecryptable
|
||||||
if (undecryptable) {
|
if (undecryptable) {
|
||||||
const pubKey = tryDecryptPublicInviteKey(rawC.invites)
|
const pubKey = tryDecryptPublicInviteKey(rawC.invites)
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
const retry = tribeCrypto.decryptContent(rawC, [[pubKey]])
|
const retry = cryptoModel.decryptContent(rawC, [[pubKey]])
|
||||||
if (retry && !retry._undecryptable) {
|
if (retry && !retry._undecryptable) {
|
||||||
c = retry
|
c = retry
|
||||||
undecryptable = false
|
undecryptable = false
|
||||||
|
|
@ -369,13 +369,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
let text = c.text || ""
|
let text = c.text || ""
|
||||||
let image = c.image || null
|
let image = c.image || null
|
||||||
let mimeType = typeof c.mimeType === "string" ? c.mimeType : null
|
let mimeType = typeof c.mimeType === "string" ? c.mimeType : null
|
||||||
if (tribeCrypto && c.encryptedText) {
|
if (cryptoModel && c.encryptedText) {
|
||||||
const candidateKeys = [...tribeKeys, ...lookupKeys(chatRootId)]
|
const candidateKeys = [...tribeKeys, ...lookupKeys(chatRootId)]
|
||||||
for (const keyHex of candidateKeys) {
|
for (const keyHex of candidateKeys) {
|
||||||
try {
|
try {
|
||||||
text = tribeCrypto.decryptWithKey(c.encryptedText, keyHex)
|
text = cryptoModel.decryptWithKey(c.encryptedText, keyHex)
|
||||||
if (c.encryptedImage) image = tribeCrypto.decryptWithKey(c.encryptedImage, keyHex)
|
if (c.encryptedImage) image = cryptoModel.decryptWithKey(c.encryptedImage, keyHex)
|
||||||
if (c.encryptedMime) mimeType = tribeCrypto.decryptWithKey(c.encryptedMime, keyHex)
|
if (c.encryptedMime) mimeType = cryptoModel.decryptWithKey(c.encryptedMime, keyHex)
|
||||||
break
|
break
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +400,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const ensureMemberKeys = async (ssbClient, messages, chats) => {
|
const ensureMemberKeys = async (ssbClient, messages, chats) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return
|
if (!cryptoModel || !ownCrypto) return
|
||||||
const leavesByRoot = new Map()
|
const leavesByRoot = new Map()
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content
|
const c = m.value && m.value.content
|
||||||
|
|
@ -421,7 +421,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(rootId)
|
const key = lookupKey(rootId)
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue
|
if (!Object.keys(memberKeys).length) continue
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -440,11 +440,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const decryptTribeChats = async (idx) => {
|
const decryptTribeChats = async (idx) => {
|
||||||
if (!tribeCrypto || !tribesModel || typeof tribeCrypto.decryptFromTribe !== "function") return idx
|
if (!cryptoModel || !tribesModel || typeof cryptoModel.decryptFromGroup !== "function") return idx
|
||||||
for (const [k, n] of idx.nodes) {
|
for (const [k, n] of idx.nodes) {
|
||||||
const c = n.c || {}
|
const c = n.c || {}
|
||||||
if (!c.encryptedPayload || !c.tribeId) continue
|
if (!c.encryptedPayload || !c.tribeId) continue
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel).catch(() => null)
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel).catch(() => null)
|
||||||
if (dec && !dec._undecryptable) idx.nodes.set(k, { ...n, c: dec })
|
if (dec && !dec._undecryptable) idx.nodes.set(k, { ...n, c: dec })
|
||||||
}
|
}
|
||||||
return idx
|
return idx
|
||||||
|
|
@ -466,7 +466,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
type: "chat",
|
type: "chat",
|
||||||
|
|
||||||
async encryptionKeyFor(chatRootId, tribeId = null) {
|
async encryptionKeyFor(chatRootId, tribeId = null) {
|
||||||
if (!tribeCrypto) return null
|
if (!cryptoModel) return null
|
||||||
if (tribeId) {
|
if (tribeId) {
|
||||||
const k = await getTribeFirstKeyFor(tribeId)
|
const k = await getTribeFirstKeyFor(tribeId)
|
||||||
if (k) return k
|
if (k) return k
|
||||||
|
|
@ -517,7 +517,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
...(extra && typeof extra === "object" && typeof extra.courseId === "string" && extra.courseId ? { courseId: extra.courseId } : {})
|
...(extra && typeof extra === "object" && typeof extra.courseId === "string" && extra.courseId ? { courseId: extra.courseId } : {})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tribeCrypto) {
|
if (!cryptoModel) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
})
|
})
|
||||||
|
|
@ -528,11 +528,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
const ancestryIds = await tribesModel.getAncestryChain(tribeId)
|
const ancestryIds = await tribesModel.getAncestryChain(tribeId)
|
||||||
const chain = []
|
const chain = []
|
||||||
for (const rid of ancestryIds || []) {
|
for (const rid of ancestryIds || []) {
|
||||||
const k = tribeCrypto.getKey(rid)
|
const k = cryptoModel.getKey(rid)
|
||||||
if (!k) { chain.length = 0; break }
|
if (!k) { chain.length = 0; break }
|
||||||
chain.push(k)
|
chain.push(k)
|
||||||
}
|
}
|
||||||
if (chain.length) content = tribeCrypto.encryptContent(content, chain, true)
|
if (chain.length) content = cryptoModel.encryptContent(content, chain, true)
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
|
|
@ -545,15 +545,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatKey = ownCrypto.generateTribeKey()
|
const chatKey = ownCrypto.generateKey()
|
||||||
content = tribeCrypto.encryptContent(content, [chatKey], true)
|
content = cryptoModel.encryptContent(content, [chatKey], true)
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
})
|
})
|
||||||
ownCrypto.setKey(result.key, chatKey, 1)
|
ownCrypto.setKey(result.key, chatKey, 1)
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const boxedKey = tribeCrypto.boxKeyForMember(chatKey, userId, ssbKeys)
|
const boxedKey = cryptoModel.boxKeyForMember(chatKey, userId, ssbKeys)
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve())
|
ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve())
|
||||||
})
|
})
|
||||||
|
|
@ -600,21 +600,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
...(chat.tribeId ? { tribeId: chat.tribeId } : {})
|
...(chat.tribeId ? { tribeId: chat.tribeId } : {})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
if (chat.tribeId) {
|
if (chat.tribeId) {
|
||||||
try {
|
try {
|
||||||
const ancestryIds = await tribesModel.getAncestryChain(chat.tribeId)
|
const ancestryIds = await tribesModel.getAncestryChain(chat.tribeId)
|
||||||
const chain = []
|
const chain = []
|
||||||
for (const rid of ancestryIds || []) {
|
for (const rid of ancestryIds || []) {
|
||||||
const k = tribeCrypto.getKey(rid)
|
const k = cryptoModel.getKey(rid)
|
||||||
if (!k) { chain.length = 0; break }
|
if (!k) { chain.length = 0; break }
|
||||||
chain.push(k)
|
chain.push(k)
|
||||||
}
|
}
|
||||||
if (chain.length) updated = tribeCrypto.encryptContent(updated, chain, true)
|
if (chain.length) updated = cryptoModel.encryptContent(updated, chain, true)
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
} else {
|
} else {
|
||||||
const chatKey = lookupKey(rootId)
|
const chatKey = lookupKey(rootId)
|
||||||
if (chatKey) updated = tribeCrypto.encryptContent(updated, [chatKey], true)
|
if (chatKey) updated = cryptoModel.encryptContent(updated, [chatKey], true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -747,16 +747,16 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
let invite = code
|
let invite = code
|
||||||
const pubFlag = opts.public ? { public: true } : {}
|
const pubFlag = opts.public ? { public: true } : {}
|
||||||
|
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt()
|
const inviteSalt = cryptoModel.generateInviteSalt()
|
||||||
const ekChain = tribeCrypto.encryptChainForInvite([chat.rootId], code, inviteSalt)
|
const ekChain = cryptoModel.encryptChainForInvite([chat.rootId], code, inviteSalt)
|
||||||
const ident = opts.public ? { code, ...pubFlag } : { ch: hashCode(code, inviteSalt) }
|
const ident = opts.public ? { code, ...pubFlag } : { ch: hashCode(code, inviteSalt) }
|
||||||
if (ekChain) {
|
if (ekChain) {
|
||||||
invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId) }
|
invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId) }
|
||||||
} else {
|
} else {
|
||||||
const chatKey = lookupKey(chat.rootId)
|
const chatKey = lookupKey(chat.rootId)
|
||||||
if (chatKey) {
|
if (chatKey) {
|
||||||
const ek = tribeCrypto.encryptForInvite(chatKey, code, inviteSalt)
|
const ek = cryptoModel.encryptForInvite(chatKey, code, inviteSalt)
|
||||||
invite = { ...ident, ek, salt: inviteSalt, gen: lookupGen(chat.rootId) }
|
invite = { ...ident, ek, salt: inviteSalt, gen: lookupGen(chat.rootId) }
|
||||||
} else {
|
} else {
|
||||||
invite = { ...ident, salt: inviteSalt }
|
invite = { ...ident, salt: inviteSalt }
|
||||||
|
|
@ -823,13 +823,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
|
|
||||||
let chatKey = null
|
let chatKey = null
|
||||||
let chain = null
|
let chain = null
|
||||||
if (tribeCrypto && typeof matchedInvite === "object") {
|
if (cryptoModel && typeof matchedInvite === "object") {
|
||||||
if (matchedInvite.ekChain) {
|
if (matchedInvite.ekChain) {
|
||||||
chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3)
|
chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3)
|
||||||
if (Array.isArray(chain) && chain.length) chatKey = chain[0].key
|
if (Array.isArray(chain) && chain.length) chatKey = chain[0].key
|
||||||
else chain = null
|
else chain = null
|
||||||
} else if (matchedInvite.ek) {
|
} else if (matchedInvite.ek) {
|
||||||
chatKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt)
|
chatKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -843,9 +843,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
if (chain) {
|
if (chain) {
|
||||||
for (const entry of chain) {
|
for (const entry of chain) {
|
||||||
if (Array.isArray(entry.keys) && entry.keys.length) {
|
if (Array.isArray(entry.keys) && entry.keys.length) {
|
||||||
tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length)
|
cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length)
|
||||||
} else if (entry.key) {
|
} else if (entry.key) {
|
||||||
tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1)
|
cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (chatKey) {
|
} else if (chatKey) {
|
||||||
|
|
@ -854,13 +854,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
|
|
||||||
await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : token)
|
await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : token)
|
||||||
|
|
||||||
if (tribeCrypto && chatKey) {
|
if (cryptoModel && chatKey) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(chatKey, userId, ssbKeys) } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(chatKey, userId, ssbKeys) } catch (_) {}
|
||||||
if (matchedChat.author && matchedChat.author !== userId) {
|
if (matchedChat.author && matchedChat.author !== userId) {
|
||||||
try { memberKeys[matchedChat.author] = tribeCrypto.boxKeyForMember(chatKey, matchedChat.author, ssbKeys) } catch (_) {}
|
try { memberKeys[matchedChat.author] = cryptoModel.boxKeyForMember(chatKey, matchedChat.author, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -881,7 +881,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
if (chat.status === "CLOSED") throw new Error("Chat is closed")
|
if (chat.status === "CLOSED") throw new Error("Chat is closed")
|
||||||
if (chat.members.includes(userId)) return chat.key
|
if (chat.members.includes(userId)) return chat.key
|
||||||
|
|
||||||
if (tribeCrypto && Array.isArray(chat.invites)) {
|
if (cryptoModel && Array.isArray(chat.invites)) {
|
||||||
const pub = chat.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain))
|
const pub = chat.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain))
|
||||||
if (pub) return await this.joinByInvite(pub.code)
|
if (pub) return await this.joinByInvite(pub.code)
|
||||||
}
|
}
|
||||||
|
|
@ -960,17 +960,17 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId)
|
const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId)
|
||||||
if (chatIsEncrypted && tribeCrypto) {
|
if (chatIsEncrypted && cryptoModel) {
|
||||||
let encKey = null
|
let encKey = null
|
||||||
if (chat.tribeId) encKey = await getTribeFirstKeyFor(chat.tribeId)
|
if (chat.tribeId) encKey = await getTribeFirstKeyFor(chat.tribeId)
|
||||||
if (!encKey) encKey = lookupKey(chat.rootId)
|
if (!encKey) encKey = lookupKey(chat.rootId)
|
||||||
if (!encKey) throw new Error(`Missing chat key for ${chat.rootId} — cannot send message`)
|
if (!encKey) throw new Error(`Missing chat key for ${chat.rootId} — cannot send message`)
|
||||||
content.encryptedText = tribeCrypto.encryptWithKey(safeText(text), encKey)
|
content.encryptedText = cryptoModel.encryptWithKey(safeText(text), encKey)
|
||||||
if (content.image) {
|
if (content.image) {
|
||||||
content.encryptedImage = tribeCrypto.encryptWithKey(String(content.image), encKey)
|
content.encryptedImage = cryptoModel.encryptWithKey(String(content.image), encKey)
|
||||||
delete content.image
|
delete content.image
|
||||||
if (content.mimeType) {
|
if (content.mimeType) {
|
||||||
content.encryptedMime = tribeCrypto.encryptWithKey(String(content.mimeType), encKey)
|
content.encryptedMime = cryptoModel.encryptWithKey(String(content.mimeType), encKey)
|
||||||
delete content.mimeType
|
delete content.mimeType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
src/models/comments_model.js
Normal file
111
src/models/comments_model.js
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
const { readTyped } = require('./typed_log');
|
||||||
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
|
const { getConfig } = require('../configs/config-manager.js');
|
||||||
|
|
||||||
|
const MSG_ID_RE = /^%[A-Za-z0-9+/]{43}=?\.sha256$/;
|
||||||
|
const ROOT_CACHE_MAX = 2000;
|
||||||
|
|
||||||
|
const isMsgId = (v) => typeof v === 'string' && MSG_ID_RE.test(v);
|
||||||
|
const isComment = (c) => !!c && typeof c === 'object' && c.type === 'post' && isMsgId(c.root) && !c.fork;
|
||||||
|
const isBlogPost = (c) => !!c && typeof c === 'object' && c.type === 'post' && typeof c.text === 'string' && !c.root && !c.fork && !c.branch && !c.about && !c.target;
|
||||||
|
const contentOf = (m) => (m && m.value && m.value.content) || (m && m.content) || null;
|
||||||
|
|
||||||
|
const commentsModel = ({ cooler, post }) => {
|
||||||
|
let ssb;
|
||||||
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
const roots = new Map();
|
||||||
|
|
||||||
|
const getMsg = async (id) => {
|
||||||
|
const client = await openSsb();
|
||||||
|
return new Promise((res) => client.get(id, (err, m) => res(err ? null : m)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const remember = (id, type) => {
|
||||||
|
if (roots.size >= ROOT_CACHE_MAX) roots.delete(roots.keys().next().value);
|
||||||
|
roots.set(id, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootTypeOf = async (rootId) => {
|
||||||
|
if (!isMsgId(rootId)) return null;
|
||||||
|
if (roots.has(rootId)) return roots.get(rootId);
|
||||||
|
const c = contentOf(await getMsg(rootId).catch(() => null));
|
||||||
|
const type = !c || typeof c !== 'object' || typeof c.type !== 'string' ? null : (c.type === 'post' && !isBlogPost(c) ? null : c.type);
|
||||||
|
if (type) remember(rootId, type);
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const annotate = async (items) => {
|
||||||
|
const list = Array.isArray(items) ? items : [];
|
||||||
|
const wanted = new Set();
|
||||||
|
for (const it of list) { const c = contentOf(it); if (isComment(c)) wanted.add(c.root); }
|
||||||
|
const types = new Map();
|
||||||
|
await Promise.all([...wanted].map(async (id) => types.set(id, await rootTypeOf(id).catch(() => null))));
|
||||||
|
for (const it of list) {
|
||||||
|
const c = contentOf(it);
|
||||||
|
if (!isComment(c)) continue;
|
||||||
|
it.comment = { rootId: c.root, rootType: types.get(c.root) || null };
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
const locate = async (id) => {
|
||||||
|
if (!isMsgId(id)) return null;
|
||||||
|
const msg = await getMsg(id).catch(() => null);
|
||||||
|
const c = contentOf(msg);
|
||||||
|
if (!c || typeof c !== 'object' || typeof c.type !== 'string') return null;
|
||||||
|
if (isComment(c)) return { commentId: id, rootId: c.root, rootType: await rootTypeOf(c.root).catch(() => null) };
|
||||||
|
if (c.type === 'post') return isBlogPost(c) ? { rootId: id, rootType: 'post' } : null;
|
||||||
|
remember(id, c.type);
|
||||||
|
return { rootId: id, rootType: c.type, author: msg.author, content: c };
|
||||||
|
};
|
||||||
|
|
||||||
|
const listFor = async (rootId) => {
|
||||||
|
if (!isMsgId(rootId)) return [];
|
||||||
|
const raw = await post.topicComments(rootId);
|
||||||
|
return (raw || []).filter(m => { const c = contentOf(m); return isComment(c) && c.root === rootId; })
|
||||||
|
.sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const listForMany = async (ids) => {
|
||||||
|
const want = new Set((ids || []).filter(isMsgId));
|
||||||
|
const out = new Map();
|
||||||
|
if (!want.size) return out;
|
||||||
|
const client = await openSsb();
|
||||||
|
const msgs = await readTyped(client, ['post', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 });
|
||||||
|
const gone = buildValidatedTombstoneSet(msgs || []);
|
||||||
|
for (const m of msgs || []) {
|
||||||
|
const c = contentOf(m);
|
||||||
|
if (!isComment(c) || gone.has(m.key) || !want.has(c.root)) continue;
|
||||||
|
if (!out.has(c.root)) out.set(c.root, []);
|
||||||
|
out.get(c.root).push(m);
|
||||||
|
}
|
||||||
|
for (const list of out.values()) list.sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0));
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countsFor = async (ids) => {
|
||||||
|
const want = new Set((ids || []).filter(isMsgId));
|
||||||
|
const counts = new Map();
|
||||||
|
if (!want.size) return counts;
|
||||||
|
const client = await openSsb();
|
||||||
|
const msgs = await readTyped(client, ['post', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 });
|
||||||
|
const gone = buildValidatedTombstoneSet(msgs || []);
|
||||||
|
for (const m of msgs || []) {
|
||||||
|
const c = contentOf(m);
|
||||||
|
if (!isComment(c) || gone.has(m.key) || !want.has(c.root)) continue;
|
||||||
|
counts.set(c.root, (counts.get(c.root) || 0) + 1);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = async ({ rootId, text }) => {
|
||||||
|
if (!isMsgId(rootId)) throw new Error('Invalid root');
|
||||||
|
return post.publish({ text, root: rootId, dest: rootId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return { isComment, isBlogPost, rootTypeOf, annotate, locate, listFor, listForMany, countsFor, publish };
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = commentsModel;
|
||||||
|
module.exports.isComment = isComment;
|
||||||
|
module.exports.isBlogPost = isBlogPost;
|
||||||
|
|
@ -27,7 +27,7 @@ const COURTS_TYPES = [
|
||||||
];
|
];
|
||||||
const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone'];
|
const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone'];
|
||||||
|
|
||||||
module.exports = ({ cooler, services = {}, tribeCrypto }) => {
|
module.exports = ({ cooler, services = {}, cryptoModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
let userId;
|
let userId;
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => {
|
||||||
for (const f of fields) {
|
for (const f of fields) {
|
||||||
if (content[f] !== undefined) payload[f] = content[f];
|
if (content[f] !== undefined) payload[f] = content[f];
|
||||||
}
|
}
|
||||||
const enc = tribeCrypto.encryptWithKey(JSON.stringify(payload), keyHex);
|
const enc = cryptoModel.encryptWithKey(JSON.stringify(payload), keyHex);
|
||||||
const result = { ...content };
|
const result = { ...content };
|
||||||
for (const f of fields) delete result[f];
|
for (const f of fields) delete result[f];
|
||||||
result.encryptedPayload = enc;
|
result.encryptedPayload = enc;
|
||||||
|
|
@ -57,7 +57,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => {
|
||||||
const decryptFields = (content, keyHex) => {
|
const decryptFields = (content, keyHex) => {
|
||||||
if (!content || !content.encryptedPayload) return content;
|
if (!content || !content.encryptedPayload) return content;
|
||||||
try {
|
try {
|
||||||
const plain = tribeCrypto.decryptWithKey(content.encryptedPayload, keyHex);
|
const plain = cryptoModel.decryptWithKey(content.encryptedPayload, keyHex);
|
||||||
const payload = JSON.parse(plain);
|
const payload = JSON.parse(plain);
|
||||||
const result = { ...content };
|
const result = { ...content };
|
||||||
delete result.encryptedPayload;
|
delete result.encryptedPayload;
|
||||||
|
|
@ -68,15 +68,15 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCaseKey = (obj) => {
|
const getCaseKey = (obj) => {
|
||||||
if (!tribeCrypto || !obj) return null;
|
if (!cryptoModel || !obj) return null;
|
||||||
return tribeCrypto.getKey(obj.rootCaseId || obj.id);
|
return cryptoModel.getKey(obj.rootCaseId || obj.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const distributeKey = async (caseKey, caseRootId, recipientId) => {
|
const distributeKey = async (caseKey, caseRootId, recipientId) => {
|
||||||
if (!tribeCrypto || !recipientId) return;
|
if (!cryptoModel || !recipientId) return;
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const ssbKeys = require('../server/node_modules/ssb-keys');
|
const ssbKeys = require('../server/node_modules/ssb-keys');
|
||||||
const boxed = tribeCrypto.boxKeyForMember(caseKey, recipientId, ssbKeys);
|
const boxed = cryptoModel.boxKeyForMember(caseKey, recipientId, ssbKeys);
|
||||||
const content = { type: 'courts-key', caseRootId, for: recipientId, memberKey: boxed };
|
const content = { type: 'courts-key', caseRootId, for: recipientId, memberKey: boxed };
|
||||||
await new Promise((res, rej) => ssbClient.publish(content, (e) => e ? rej(e) : res()));
|
await new Promise((res, rej) => ssbClient.publish(content, (e) => e ? rej(e) : res()));
|
||||||
};
|
};
|
||||||
|
|
@ -182,15 +182,15 @@ if (c.type === type) {
|
||||||
createdAt: openedAt
|
createdAt: openedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const stub = { ...content };
|
const stub = { ...content };
|
||||||
for (const f of CASE_FIELDS) delete stub[f];
|
for (const f of CASE_FIELDS) delete stub[f];
|
||||||
const initialMsg = await new Promise((res, rej) =>
|
const initialMsg = await new Promise((res, rej) =>
|
||||||
ssbClient.publish(stub, (err, msg) => (err ? rej(err) : res(msg)))
|
ssbClient.publish(stub, (err, msg) => (err ? rej(err) : res(msg)))
|
||||||
);
|
);
|
||||||
const caseRootId = initialMsg.key;
|
const caseRootId = initialMsg.key;
|
||||||
const caseKey = tribeCrypto.generateTribeKey();
|
const caseKey = cryptoModel.generateKey();
|
||||||
tribeCrypto.setKey(caseRootId, caseKey, 1);
|
cryptoModel.setKey(caseRootId, caseKey, 1);
|
||||||
const encrypted = encryptFields({ ...content, rootCaseId: caseRootId }, caseKey, CASE_FIELDS);
|
const encrypted = encryptFields({ ...content, rootCaseId: caseRootId }, caseKey, CASE_FIELDS);
|
||||||
const update = { ...encrypted, replaces: caseRootId, updatedAt: openedAt };
|
const update = { ...encrypted, replaces: caseRootId, updatedAt: openedAt };
|
||||||
const finalMsg = await new Promise((res, rej) =>
|
const finalMsg = await new Promise((res, rej) =>
|
||||||
|
|
@ -317,7 +317,7 @@ if (c.type === type) {
|
||||||
const node = nodes.get(tip);
|
const node = nodes.get(tip);
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
let c = { id: tip, ...node.c };
|
let c = { id: tip, ...node.c };
|
||||||
if (tribeCrypto) { const key = getCaseKey(c); if (key) c = decryptFields(c, key); }
|
if (cryptoModel) { const key = getCaseKey(c); if (key) c = decryptFields(c, key); }
|
||||||
if (!c.rootCaseId) c.rootCaseId = root;
|
if (!c.rootCaseId) c.rootCaseId = root;
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +344,7 @@ if (c.type === type) {
|
||||||
const author = String(m.value?.author || '');
|
const author = String(m.value?.author || '');
|
||||||
const ts = m.value?.timestamp || m.timestamp || 0;
|
const ts = m.value?.timestamp || m.timestamp || 0;
|
||||||
if (c.type === 'courtsMediators' && matchC(c)) {
|
if (c.type === 'courtsMediators' && matchC(c)) {
|
||||||
let dec = c; if (tribeCrypto && caseKey) dec = decryptFields(c, caseKey);
|
let dec = c; if (cryptoModel && caseKey) dec = decryptFields(c, caseKey);
|
||||||
const list = ensureArray(dec.mediators || []).map(x => String(x || '').trim()).filter(Boolean).filter(x => x !== accuser && x !== respondentId);
|
const list = ensureArray(dec.mediators || []).map(x => String(x || '').trim()).filter(Boolean).filter(x => x !== accuser && x !== respondentId);
|
||||||
if (c.side === 'accuser' && author === accuser && ts >= medATs) { medA = list; medATs = ts; }
|
if (c.side === 'accuser' && author === accuser && ts >= medATs) { medA = list; medATs = ts; }
|
||||||
else if (c.side === 'respondent' && author === respondentId && ts >= medRTs) { medR = list; medRTs = ts; }
|
else if (c.side === 'respondent' && author === respondentId && ts >= medRTs) { medR = list; medRTs = ts; }
|
||||||
|
|
@ -454,7 +454,7 @@ if (c.type === type) {
|
||||||
);
|
);
|
||||||
const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId);
|
const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId);
|
||||||
const caseRootId = c.rootCaseId || c.id;
|
const caseRootId = c.rootCaseId || c.id;
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) {
|
if (caseKey) {
|
||||||
for (const mediatorId of clean) {
|
for (const mediatorId of clean) {
|
||||||
|
|
@ -464,7 +464,7 @@ if (c.type === type) {
|
||||||
}
|
}
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
let content = { type: 'courtsMediators', caseId: caseRootId, side, mediators: clean, author: userId, createdAt: nowISO() };
|
let content = { type: 'courtsMediators', caseId: caseRootId, side, mediators: clean, author: userId, createdAt: nowISO() };
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) content = encryptFields(content, caseKey, MEDIATORS_FIELDS);
|
if (caseKey) content = encryptFields(content, caseKey, MEDIATORS_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
@ -488,7 +488,7 @@ if (c.type === type) {
|
||||||
throw new Error('Judge cannot be a party of the case.');
|
throw new Error('Judge cannot be a party of the case.');
|
||||||
}
|
}
|
||||||
const caseRootId = c.rootCaseId || c.id;
|
const caseRootId = c.rootCaseId || c.id;
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) {
|
if (caseKey) {
|
||||||
await distributeKey(caseKey, caseRootId, id);
|
await distributeKey(caseKey, caseRootId, id);
|
||||||
|
|
@ -525,7 +525,7 @@ if (c.type === type) {
|
||||||
imageUrl,
|
imageUrl,
|
||||||
createdAt: nowISO()
|
createdAt: nowISO()
|
||||||
};
|
};
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) content = encryptFields(content, caseKey, EVIDENCE_FIELDS);
|
if (caseKey) content = encryptFields(content, caseKey, EVIDENCE_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
@ -552,7 +552,7 @@ if (c.type === type) {
|
||||||
text: t,
|
text: t,
|
||||||
createdAt: nowISO()
|
createdAt: nowISO()
|
||||||
};
|
};
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) content = encryptFields(content, caseKey, ANSWER_FIELDS);
|
if (caseKey) content = encryptFields(content, caseKey, ANSWER_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
@ -592,7 +592,7 @@ if (c.type === type) {
|
||||||
orders: o,
|
orders: o,
|
||||||
createdAt: nowISO()
|
createdAt: nowISO()
|
||||||
};
|
};
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) content = encryptFields(content, caseKey, VERDICT_FIELDS);
|
if (caseKey) content = encryptFields(content, caseKey, VERDICT_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
@ -620,7 +620,7 @@ if (c.type === type) {
|
||||||
terms: t,
|
terms: t,
|
||||||
createdAt: nowISO()
|
createdAt: nowISO()
|
||||||
};
|
};
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
const caseKey = getCaseKey(c);
|
const caseKey = getCaseKey(c);
|
||||||
if (caseKey) content = encryptFields(content, caseKey, SETTLEMENT_FIELDS);
|
if (caseKey) content = encryptFields(content, caseKey, SETTLEMENT_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
@ -972,7 +972,7 @@ if (c.type === type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processIncomingCourtsKeys() {
|
async function processIncomingCourtsKeys() {
|
||||||
if (!tribeCrypto) return;
|
if (!cryptoModel) return;
|
||||||
const ssbKeys = require('../server/node_modules/ssb-keys');
|
const ssbKeys = require('../server/node_modules/ssb-keys');
|
||||||
const ssbConfig = require('../server/ssb_config');
|
const ssbConfig = require('../server/ssb_config');
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
|
|
@ -982,10 +982,10 @@ if (c.type === type) {
|
||||||
if (!c || c.type !== 'courts-key') continue;
|
if (!c || c.type !== 'courts-key') continue;
|
||||||
if (c.for !== ssbClient.id) continue;
|
if (c.for !== ssbClient.id) continue;
|
||||||
if (!c.memberKey || !c.caseRootId) continue;
|
if (!c.memberKey || !c.caseRootId) continue;
|
||||||
if (tribeCrypto.getKey(c.caseRootId)) continue;
|
if (cryptoModel.getKey(c.caseRootId)) continue;
|
||||||
try {
|
try {
|
||||||
const key = tribeCrypto.unboxKeyFromMember(c.memberKey, ssbConfig.keys, ssbKeys);
|
const key = cryptoModel.unboxKeyFromMember(c.memberKey, ssbConfig.keys, ssbKeys);
|
||||||
if (key) tribeCrypto.setKey(c.caseRootId, key, 1);
|
if (key) cryptoModel.setKey(c.caseRootId, key, 1);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,14 @@ const ENVELOPE_VERSION = 1;
|
||||||
const KEY_DISTRIB_TYPE = 'tribe-keys-distrib';
|
const KEY_DISTRIB_TYPE = 'tribe-keys-distrib';
|
||||||
const KEY_DISTRIB_BATCH = 7;
|
const KEY_DISTRIB_BATCH = 7;
|
||||||
|
|
||||||
module.exports = (configPath, namespace = 'tribes') => {
|
const SHARED_SCOPE = 'tribes';
|
||||||
|
|
||||||
|
const createScope = (configPath, namespace) => {
|
||||||
const keysDir = require('../configs/state-manager').keysDir(configPath);
|
const keysDir = require('../configs/state-manager').keysDir(configPath);
|
||||||
try { fs.mkdirSync(keysDir, { recursive: true, mode: 0o700 }); } catch (_) {}
|
try { fs.mkdirSync(keysDir, { recursive: true, mode: 0o700 }); } catch (_) {}
|
||||||
const keyringPath = path.join(keysDir, `${namespace}-keys.json`);
|
const keyringPath = path.join(keysDir, `${namespace}-keys.json`);
|
||||||
|
|
||||||
if (namespace === 'tribes') {
|
if (namespace === SHARED_SCOPE) {
|
||||||
const legacyPath = path.join(configPath, 'tribe-keys.json');
|
const legacyPath = path.join(configPath, 'tribe-keys.json');
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(legacyPath) && !fs.existsSync(keyringPath)) {
|
if (fs.existsSync(legacyPath) && !fs.existsSync(keyringPath)) {
|
||||||
|
|
@ -65,7 +67,7 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
try { fs.chmodSync(keyringPath, 0o600); } catch (_) {}
|
try { fs.chmodSync(keyringPath, 0o600); } catch (_) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateTribeKey = () => crypto.randomBytes(32).toString('hex');
|
const generateKey = () => crypto.randomBytes(32).toString('hex');
|
||||||
const HEX_KEY = /^[0-9a-f]{64}$/i;
|
const HEX_KEY = /^[0-9a-f]{64}$/i;
|
||||||
const validKey = (k) => typeof k === 'string' && HEX_KEY.test(k);
|
const validKey = (k) => typeof k === 'string' && HEX_KEY.test(k);
|
||||||
|
|
||||||
|
|
@ -198,7 +200,7 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTribeMsg = (c) =>
|
const isSealedMsg = (c) =>
|
||||||
!!c && c.type === ENVELOPE_TYPE && c.v === ENVELOPE_VERSION && typeof c.fp === 'string' && typeof c.p === 'string';
|
!!c && c.type === ENVELOPE_TYPE && c.v === ENVELOPE_VERSION && typeof c.fp === 'string' && typeof c.p === 'string';
|
||||||
|
|
||||||
const wrapMsg = (body, kHex) => {
|
const wrapMsg = (body, kHex) => {
|
||||||
|
|
@ -210,7 +212,7 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const unwrapMsg = (envelope, fpIdx) => {
|
const unwrapMsg = (envelope, fpIdx) => {
|
||||||
if (!isTribeMsg(envelope)) return null;
|
if (!isSealedMsg(envelope)) return null;
|
||||||
const aad = buildAad(envelope.fp);
|
const aad = buildAad(envelope.fp);
|
||||||
const tryKey = (rootId, keyHex) => {
|
const tryKey = (rootId, keyHex) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -495,15 +497,15 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
return buildKeyChainSets(ancestryIds);
|
return buildKeyChainSets(ancestryIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptForTribe = async (content, tribeId, tribesModel) => {
|
const encryptForGroup = async (content, tribeId, tribesModel) => {
|
||||||
const chain = await resolveKeyChain(tribeId, tribesModel);
|
const chain = await resolveKeyChain(tribeId, tribesModel);
|
||||||
if (!chain) throw new Error('Missing tribe key chain — cannot encrypt content for this tribe');
|
if (!chain) throw new Error('Missing key chain — cannot encrypt this content');
|
||||||
return encryptContent(content, chain, true);
|
return encryptContent(content, chain, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptFromTribe = async (content, tribesModel) => {
|
const decryptFromGroup = async (content, tribesModel) => {
|
||||||
if (!content) return content;
|
if (!content) return content;
|
||||||
if (isTribeMsg(content)) {
|
if (isSealedMsg(content)) {
|
||||||
const fpIdx = buildFingerprintIndex();
|
const fpIdx = buildFingerprintIndex();
|
||||||
const r = unwrapMsg(content, fpIdx);
|
const r = unwrapMsg(content, fpIdx);
|
||||||
if (!r) return { ...content, _undecryptable: true };
|
if (!r) return { ...content, _undecryptable: true };
|
||||||
|
|
@ -539,17 +541,17 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHelpers = (tribesModel) => ({
|
const createHelpers = (tribesModel) => ({
|
||||||
async encryptIfTribe(content) {
|
async encryptIfGroup(content) {
|
||||||
if (!content || !content.tribeId || !tribesModel) return content;
|
if (!content || !content.tribeId || !tribesModel) return content;
|
||||||
const rootId = await tribesModel.getRootId(content.tribeId);
|
const rootId = await tribesModel.getRootId(content.tribeId);
|
||||||
const key = getKey(rootId);
|
const key = getKey(rootId);
|
||||||
if (!key) throw new Error(`Missing tribe key for ${rootId} — cannot publish encrypted content`);
|
if (!key) throw new Error(`Missing key for ${rootId} — cannot publish encrypted content`);
|
||||||
const body = { k: content.type, ...content };
|
const body = { k: content.type, ...content };
|
||||||
return wrapMsg(body, key);
|
return wrapMsg(body, key);
|
||||||
},
|
},
|
||||||
async decryptIfTribe(content) {
|
async decryptIfGroup(content) {
|
||||||
if (!content || !tribesModel) return content;
|
if (!content || !tribesModel) return content;
|
||||||
if (isTribeMsg(content)) {
|
if (isSealedMsg(content)) {
|
||||||
const fpIdx = buildFingerprintIndex();
|
const fpIdx = buildFingerprintIndex();
|
||||||
const r = unwrapMsg(content, fpIdx);
|
const r = unwrapMsg(content, fpIdx);
|
||||||
if (!r) return { ...content, _undecryptable: true };
|
if (!r) return { ...content, _undecryptable: true };
|
||||||
|
|
@ -560,17 +562,17 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
return flat;
|
return flat;
|
||||||
}
|
}
|
||||||
if (!content.encryptedPayload) return content;
|
if (!content.encryptedPayload) return content;
|
||||||
return await decryptFromTribe(content, tribesModel);
|
return await decryptFromGroup(content, tribesModel);
|
||||||
},
|
},
|
||||||
assertReadable(decrypted, what) {
|
assertReadable(decrypted, what) {
|
||||||
if (decrypted && decrypted._undecryptable) throw new Error(`${what} is tribe-encrypted and cannot be decrypted with available keys`);
|
if (decrypted && decrypted._undecryptable) throw new Error(`${what} is encrypted and cannot be decrypted with available keys`);
|
||||||
},
|
},
|
||||||
async decryptIndexNodes(idx) {
|
async decryptIndexNodes(idx) {
|
||||||
if (!tribesModel) return;
|
if (!tribesModel) return;
|
||||||
for (const [k, n] of idx.nodes.entries()) {
|
for (const [k, n] of idx.nodes.entries()) {
|
||||||
if (!n.c) continue;
|
if (!n.c) continue;
|
||||||
if (!n.c.encryptedPayload && !isTribeMsg(n.c)) continue;
|
if (!n.c.encryptedPayload && !isSealedMsg(n.c)) continue;
|
||||||
const dec = await decryptFromTribe(n.c, tribesModel);
|
const dec = await decryptFromGroup(n.c, tribesModel);
|
||||||
if (dec && !dec._undecryptable) {
|
if (dec && !dec._undecryptable) {
|
||||||
idx.nodes.set(k, { ...n, c: { ...dec, _decrypted: true } });
|
idx.nodes.set(k, { ...n, c: { ...dec, _decrypted: true } });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -586,7 +588,7 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m && m.value && m.value.content;
|
const c = m && m.value && m.value.content;
|
||||||
if (!c) continue;
|
if (!c) continue;
|
||||||
if (!isTribeMsg(c)) { out.push(m); continue; }
|
if (!isSealedMsg(c)) { out.push(m); continue; }
|
||||||
const r = unwrapMsg(c, fpIdx);
|
const r = unwrapMsg(c, fpIdx);
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const inner = r.body;
|
const inner = r.body;
|
||||||
|
|
@ -606,7 +608,7 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
if (!tribeId || !tribesModel) return { type: 'tombstone', target, deletedAt, author };
|
if (!tribeId || !tribesModel) return { type: 'tombstone', target, deletedAt, author };
|
||||||
const rootId = await tribesModel.getRootId(tribeId);
|
const rootId = await tribesModel.getRootId(tribeId);
|
||||||
const key = getKey(rootId);
|
const key = getKey(rootId);
|
||||||
if (!key) throw new Error(`Missing tribe key for ${rootId} — cannot publish encrypted tombstone`);
|
if (!key) throw new Error(`Missing key for ${rootId} — cannot publish encrypted tombstone`);
|
||||||
return wrapMsg({ k: 'tombstone', target, deletedAt, author }, key);
|
return wrapMsg({ k: 'tombstone', target, deletedAt, author }, key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -617,10 +619,10 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
SENSITIVE_FIELDS, ENVELOPE_PRESERVE,
|
SENSITIVE_FIELDS, ENVELOPE_PRESERVE,
|
||||||
ENVELOPE_TYPE, ENVELOPE_VERSION, KEY_DISTRIB_TYPE, KEY_DISTRIB_BATCH,
|
ENVELOPE_TYPE, ENVELOPE_VERSION, KEY_DISTRIB_TYPE, KEY_DISTRIB_BATCH,
|
||||||
loadKeyring, saveKeyring,
|
loadKeyring, saveKeyring,
|
||||||
generateTribeKey, getKey, getKeys, getGen, getAllRootIds,
|
generateKey, getKey, getKeys, getGen, getAllRootIds,
|
||||||
setKey, setKeys, mergeKeys, addNewKey, dropKey, keyPlan,
|
setKey, setKeys, mergeKeys, addNewKey, dropKey, keyPlan,
|
||||||
fingerprint, buildFingerprintIndex,
|
fingerprint, buildFingerprintIndex,
|
||||||
isTribeMsg, wrapMsg, unwrapMsg,
|
isSealedMsg, wrapMsg, unwrapMsg,
|
||||||
encryptWithKey, decryptWithKey,
|
encryptWithKey, decryptWithKey,
|
||||||
generateInviteSalt, hashInviteCode, deriveInviteKey,
|
generateInviteSalt, hashInviteCode, deriveInviteKey,
|
||||||
encryptForInvite, decryptFromInvite,
|
encryptForInvite, decryptFromInvite,
|
||||||
|
|
@ -630,7 +632,16 @@ module.exports = (configPath, namespace = 'tribes') => {
|
||||||
canonicalAad, encryptChain, decryptChain,
|
canonicalAad, encryptChain, decryptChain,
|
||||||
encryptContent, decryptContent,
|
encryptContent, decryptContent,
|
||||||
buildKeyChainSets, resolveKeyChain, resolveKeyChainSets,
|
buildKeyChainSets, resolveKeyChain, resolveKeyChainSets,
|
||||||
encryptForTribe, decryptFromTribe,
|
encryptForGroup, decryptFromGroup,
|
||||||
createHelpers
|
createHelpers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = (configPath) => {
|
||||||
|
const scopes = new Map();
|
||||||
|
const scope = (namespace = SHARED_SCOPE) => {
|
||||||
|
if (!scopes.has(namespace)) scopes.set(namespace, createScope(configPath, namespace));
|
||||||
|
return scopes.get(namespace);
|
||||||
|
};
|
||||||
|
return { ...scope(SHARED_SCOPE), scope };
|
||||||
|
};
|
||||||
|
|
@ -165,6 +165,7 @@ module.exports = ({ cooler, favoriteIdsFor = null }) => {
|
||||||
const vg = versionGroups(versions);
|
const vg = versionGroups(versions);
|
||||||
const latestByRoot = new Map();
|
const latestByRoot = new Map();
|
||||||
for (const root of vg.roots) {
|
for (const root of vg.roots) {
|
||||||
|
if (tomb.has(root) || vg.isForeignCopy(root)) continue;
|
||||||
const tip = candidates.get(vg.tipOf(root));
|
const tip = candidates.get(vg.tipOf(root));
|
||||||
if (!tip) continue;
|
if (!tip) continue;
|
||||||
if (tip.kind !== 'tribes') { latestByRoot.set(root, { ...tip, key: root }); continue; }
|
if (tip.kind !== 'tribes') { latestByRoot.set(root, { ...tip, key: root }); continue; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const categories = require("../backend/opinion_categories");
|
const categories = require("../backend/opinion_categories");
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
|
|
@ -107,12 +108,6 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasBlob = (ssbClient, blobId) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
if (!blobId) return resolve(false);
|
|
||||||
ssbClient.blobs.has(blobId, (err, has) => resolve(!err && !!has));
|
|
||||||
});
|
|
||||||
|
|
||||||
const favoritesSetForDocuments = async () => {
|
const favoritesSetForDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
return await contentFavorites.getFavoriteSet("documents");
|
return await contentFavorites.getFavoriteSet("documents");
|
||||||
|
|
@ -180,7 +175,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (oldMsg.content?.type !== "document") throw new Error("Document not found");
|
if (oldMsg.content?.type !== "document") throw new Error("Document not found");
|
||||||
if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author");
|
if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author");
|
||||||
const aggDoc = await this.getDocumentById(id);
|
const aggDoc = await this.getDocumentById(id);
|
||||||
if (aggDoc && Object.keys(aggDoc.opinions || {}).some(k => (aggDoc.opinions[k] || 0) > 0)) throw new Error("Cannot edit document after it has received opinions.");
|
opinionRules.assertMediaKept(aggDoc, !!blobMarkdown);
|
||||||
|
|
||||||
const parsedTags = parseCSV(tagsRaw);
|
const parsedTags = parseCSV(tagsRaw);
|
||||||
const tags = parsedTags !== undefined ? parsedTags : safeArr(oldMsg.content.tags);
|
const tags = parsedTags !== undefined ? parsedTags : safeArr(oldMsg.content.tags);
|
||||||
|
|
@ -277,10 +272,7 @@ module.exports = ({ cooler }) => {
|
||||||
out = out.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
out = out.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
const checked = await Promise.all(out.map(async (d) => ((await hasBlob(ssbClient, d.url)) ? d : null)));
|
return out.map((d) => ({ ...d, isFavorite: favorites.has(d.rootId || d.key) }));
|
||||||
return checked
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((d) => ({ ...d, isFavorite: favorites.has(d.rootId || d.key) }));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDocumentById(id) {
|
async getDocumentById(id) {
|
||||||
|
|
|
||||||
|
|
@ -16,28 +16,28 @@ const EVENT_TYPES = [
|
||||||
'tribe-keys', 'tombstone'
|
'tribe-keys', 'tombstone'
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, eventCrypto, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
const me = async () => (await openSsb()).id;
|
const me = async () => (await openSsb()).id;
|
||||||
|
|
||||||
const ownCrypto = eventCrypto || tribeCrypto;
|
const ownCrypto = eventCrypto || cryptoModel;
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null;
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null;
|
||||||
|
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0);
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0);
|
||||||
|
|
||||||
const readAll = async (ssbClient) =>
|
const readAll = async (ssbClient) =>
|
||||||
readTyped(ssbClient, EVENT_TYPES, { limit: logLimit });
|
readTyped(ssbClient, EVENT_TYPES, { limit: logLimit });
|
||||||
|
|
||||||
const rotateEventKey = async (rid, attendees) => {
|
const rotateEventKey = async (rid, attendees) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rid || !lookupKey(rid)) return;
|
if (!ownCrypto || !cryptoModel || !rid || !lookupKey(rid)) return;
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const newKey = ownCrypto.generateTribeKey();
|
const newKey = ownCrypto.generateKey();
|
||||||
const newGen = ownCrypto.addNewKey(rid, newKey);
|
const newGen = ownCrypto.addNewKey(rid, newKey);
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(attendees) ? attendees : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(attendees) ? attendees : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve());
|
ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve());
|
||||||
|
|
@ -45,7 +45,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureAttendeeKeys = async (ssbClient, messages, events) => {
|
const ensureAttendeeKeys = async (ssbClient, messages, events) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return;
|
if (!cryptoModel || !ownCrypto) return;
|
||||||
const leavesByRoot = new Map();
|
const leavesByRoot = new Map();
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content;
|
const c = m.value && m.value.content;
|
||||||
|
|
@ -63,7 +63,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(ev.rid);
|
const key = lookupKey(ev.rid);
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys); } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue;
|
if (!Object.keys(memberKeys).length) continue;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -100,10 +100,10 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const decryptEventContent = (rawContent, eventId) => {
|
const decryptEventContent = (rawContent, eventId) => {
|
||||||
if (!rawContent) return null;
|
if (!rawContent) return null;
|
||||||
if (!rawContent.encryptedPayload) return rawContent;
|
if (!rawContent.encryptedPayload) return rawContent;
|
||||||
if (!ownCrypto || !tribeCrypto) return { ...rawContent, _undecryptable: true };
|
if (!ownCrypto || !cryptoModel) return { ...rawContent, _undecryptable: true };
|
||||||
const keys = (ownCrypto.getKeys && ownCrypto.getKeys(eventId)) || [];
|
const keys = (ownCrypto.getKeys && ownCrypto.getKeys(eventId)) || [];
|
||||||
if (!keys.length) return { ...rawContent, _undecryptable: true };
|
if (!keys.length) return { ...rawContent, _undecryptable: true };
|
||||||
const dec = tribeCrypto.decryptContent(rawContent, keys.map(k => [k]));
|
const dec = cryptoModel.decryptContent(rawContent, keys.map(k => [k]));
|
||||||
if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true };
|
if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true };
|
||||||
return { ...dec, _decrypted: true };
|
return { ...dec, _decrypted: true };
|
||||||
};
|
};
|
||||||
|
|
@ -256,12 +256,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
opinions_inhabitants: []
|
opinions_inhabitants: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldEncrypt = visibility === 'private' && ownCrypto && tribeCrypto;
|
const shouldEncrypt = visibility === 'private' && ownCrypto && cryptoModel;
|
||||||
let eventKey = null;
|
let eventKey = null;
|
||||||
let content = plainContent;
|
let content = plainContent;
|
||||||
if (shouldEncrypt) {
|
if (shouldEncrypt) {
|
||||||
eventKey = ownCrypto.generateTribeKey();
|
eventKey = ownCrypto.generateKey();
|
||||||
content = tribeCrypto.encryptContent(plainContent, [eventKey], true);
|
content = cryptoModel.encryptContent(plainContent, [eventKey], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
|
@ -274,7 +274,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of attendeeList) {
|
for (const m of attendeeList) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(eventKey, m, ssbKeys); } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(eventKey, m, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -287,7 +287,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateInvite(eventId) {
|
async generateInvite(eventId) {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable");
|
if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable");
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const userId = await me();
|
const userId = await me();
|
||||||
const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m)));
|
const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m)));
|
||||||
|
|
@ -299,11 +299,11 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(rid);
|
const key = lookupKey(rid);
|
||||||
if (!key) throw new Error("Missing event key — cannot generate invite");
|
if (!key) throw new Error("Missing event key — cannot generate invite");
|
||||||
const code = crypto.randomBytes(16).toString('hex');
|
const code = crypto.randomBytes(16).toString('hex');
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt);
|
const ek = cryptoModel.encryptForInvite(key, code, inviteSalt);
|
||||||
const inviteRef = { code, ek, salt: inviteSalt, gen: 1, rootId: rid };
|
const inviteRef = { code, ek, salt: inviteSalt, gen: 1, rootId: rid };
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve());
|
ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve());
|
||||||
});
|
});
|
||||||
return { code, eventId: rid };
|
return { code, eventId: rid };
|
||||||
},
|
},
|
||||||
|
|
@ -339,7 +339,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateOpenInvite(eventId) {
|
async generateOpenInvite(eventId) {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable");
|
if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable");
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const userId = await me();
|
const userId = await me();
|
||||||
const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m)));
|
const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m)));
|
||||||
|
|
@ -352,10 +352,10 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(rid);
|
const key = lookupKey(rid);
|
||||||
if (!key) throw new Error("Missing event key — cannot generate invite");
|
if (!key) throw new Error("Missing event key — cannot generate invite");
|
||||||
const code = crypto.randomBytes(16).toString('hex');
|
const code = crypto.randomBytes(16).toString('hex');
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt);
|
const ek = cryptoModel.encryptForInvite(key, code, inviteSalt);
|
||||||
const invitePub = await new Promise((resolve, reject) => {
|
const invitePub = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r));
|
ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r));
|
||||||
});
|
});
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'event-open-invite', v: 1, target: rid, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve());
|
ssbClient.publish({ type: 'event-open-invite', v: 1, target: rid, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve());
|
||||||
|
|
@ -391,7 +391,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async joinByInvite(code) {
|
async joinByInvite(code) {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable");
|
if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable");
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const userId = await me();
|
const userId = await me();
|
||||||
const messages = await readAll(ssbClient);
|
const messages = await readAll(ssbClient);
|
||||||
|
|
@ -406,12 +406,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
if (!c || c.type !== 'event-invite') continue;
|
if (!c || c.type !== 'event-invite') continue;
|
||||||
if (invTomb.has(m.key)) continue;
|
if (invTomb.has(m.key)) continue;
|
||||||
try {
|
try {
|
||||||
const hash = tribeCrypto.hashInviteCode(code, c.salt);
|
const hash = cryptoModel.hashInviteCode(code, c.salt);
|
||||||
if (hash === c.codeHash) { matched = c; break; }
|
if (hash === c.codeHash) { matched = c; break; }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!matched) throw new Error("Invalid or expired invite code");
|
if (!matched) throw new Error("Invalid or expired invite code");
|
||||||
const eventKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt);
|
const eventKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt);
|
||||||
if (!eventKey) throw new Error("Could not decrypt invite");
|
if (!eventKey) throw new Error("Could not decrypt invite");
|
||||||
ownCrypto.addNewKey(matched.target, eventKey);
|
ownCrypto.addNewKey(matched.target, eventKey);
|
||||||
await this.toggleAttendee(matched.target);
|
await this.toggleAttendee(matched.target);
|
||||||
|
|
@ -444,13 +444,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
ssbClient.publish({ type: 'eventAttend', target: rid, on, createdAt: new Date().toISOString() }, (err2, res2) => err2 ? reject(err2) : resolve(res2));
|
ssbClient.publish({ type: 'eventAttend', target: rid, on, createdAt: new Date().toISOString() }, (err2, res2) => err2 ? reject(err2) : resolve(res2));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPrivate && !isLeaving && ownCrypto && tribeCrypto) {
|
if (isPrivate && !isLeaving && ownCrypto && cryptoModel) {
|
||||||
try {
|
try {
|
||||||
const key = lookupKey(rid);
|
const key = lookupKey(rid);
|
||||||
if (key) {
|
if (key) {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(key, userId, ssbKeys); } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(key, userId, ssbKeys); } catch (_) {}
|
||||||
if (memberKeys[userId]) {
|
if (memberKeys[userId]) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: 1, memberKeys }, () => resolve());
|
ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: 1, memberKeys }, () => resolve());
|
||||||
|
|
@ -498,6 +498,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: contentTip,
|
id: contentTip,
|
||||||
|
rootId: rid,
|
||||||
title: c.title || '',
|
title: c.title || '',
|
||||||
description: c.description || '',
|
description: c.description || '',
|
||||||
date: c.date || '',
|
date: c.date || '',
|
||||||
|
|
@ -578,26 +579,26 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => {
|
||||||
const wasPrivate = normalizePrivacy(c.isPublic) === 'private';
|
const wasPrivate = normalizePrivacy(c.isPublic) === 'private';
|
||||||
const isPrivate = updated.isPublic === 'private';
|
const isPrivate = updated.isPublic === 'private';
|
||||||
let newKey = null;
|
let newKey = null;
|
||||||
if (isPrivate && ownCrypto && tribeCrypto) {
|
if (isPrivate && ownCrypto && cryptoModel) {
|
||||||
let key = lookupKey(rid);
|
let key = lookupKey(rid);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
newKey = ownCrypto.generateTribeKey();
|
newKey = ownCrypto.generateKey();
|
||||||
ownCrypto.setKey(rid, newKey, 1);
|
ownCrypto.setKey(rid, newKey, 1);
|
||||||
key = newKey;
|
key = newKey;
|
||||||
}
|
}
|
||||||
updated = tribeCrypto.encryptContent(updated, [key], true);
|
updated = cryptoModel.encryptContent(updated, [key], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(updated, (err2, res2) => err2 ? reject(err2) : resolve(res2));
|
ssbClient.publish(updated, (err2, res2) => err2 ? reject(err2) : resolve(res2));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newKey && tribeCrypto) {
|
if (newKey && cryptoModel) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of (Array.isArray(c.attendees) ? c.attendees : [userId])) {
|
for (const m of (Array.isArray(c.attendees) ? c.attendees : [userId])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ module.exports = ({ cooler }) => {
|
||||||
|
|
||||||
const cleanText = (t) => (typeof t === "string" ? t.replace(/\r\n?/g, "\n").trim() : "");
|
const cleanText = (t) => (typeof t === "string" ? t.replace(/\r\n?/g, "\n").trim() : "");
|
||||||
|
|
||||||
|
const FEED_MEDIA_SRC = String.raw`!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)`;
|
||||||
const isValidFeedText = (t) => {
|
const isValidFeedText = (t) => {
|
||||||
const s = cleanText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, "").trim();
|
const all = cleanText(t);
|
||||||
return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX;
|
const s = all.replace(new RegExp(FEED_MEDIA_SRC, "g"), "").trim();
|
||||||
|
if (s.length > FEED_TEXT_MAX) return false;
|
||||||
|
return s.length >= FEED_TEXT_MIN || (!s && new RegExp(FEED_MEDIA_SRC).test(all));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMsg = (ssbClient, id) =>
|
const getMsg = (ssbClient, id) =>
|
||||||
|
|
@ -100,16 +103,18 @@ module.exports = ({ cooler }) => {
|
||||||
|
|
||||||
if (typeof text !== "string") throw new Error("Invalid text");
|
if (typeof text !== "string") throw new Error("Invalid text");
|
||||||
const cleaned = cleanText(text);
|
const cleaned = cleanText(text);
|
||||||
|
const attached = typeof media === "string" ? media.trim() : "";
|
||||||
|
const full = [cleaned, attached].filter(Boolean).join("\n");
|
||||||
|
|
||||||
if (!isValidFeedText(cleaned)) {
|
if (!full) throw new Error("Text required");
|
||||||
if (cleaned.length < FEED_TEXT_MIN) throw new Error("Text too short");
|
if (!isValidFeedText(full)) {
|
||||||
if (cleaned.length > FEED_TEXT_MAX) throw new Error("Text too long");
|
if (cleaned.length > FEED_TEXT_MAX) throw new Error("Text too long");
|
||||||
throw new Error("Text required");
|
throw new Error("Text too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = {
|
const content = {
|
||||||
type: "feed",
|
type: "feed",
|
||||||
text: typeof media === "string" && media.trim() ? `${cleaned}\n${media.trim()}` : cleaned,
|
text: full,
|
||||||
author: userId,
|
author: userId,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
tags: extractTags(cleaned),
|
tags: extractTags(cleaned),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const categories = require("../backend/opinion_categories");
|
const categories = require("../backend/opinion_categories");
|
||||||
const { readTyped, memoIndex } = require("./typed_log");
|
const { readTyped, memoIndex } = require("./typed_log");
|
||||||
|
|
@ -19,6 +20,7 @@ const fileExtra = (extra) => {
|
||||||
const out = {};
|
const out = {};
|
||||||
if (extra.mime) out.mime = String(extra.mime);
|
if (extra.mime) out.mime = String(extra.mime);
|
||||||
if (extra.fileName) out.fileName = String(extra.fileName);
|
if (extra.fileName) out.fileName = String(extra.fileName);
|
||||||
|
if (typeof extra.license === "string" && extra.license) out.license = extra.license;
|
||||||
if (typeof extra.torrentKey === "string" && extra.torrentKey.startsWith("%")) out.torrentKey = extra.torrentKey;
|
if (typeof extra.torrentKey === "string" && extra.torrentKey.startsWith("%")) out.torrentKey = extra.torrentKey;
|
||||||
if (typeof extra.torrentUrl === "string" && extra.torrentUrl.startsWith("&")) out.torrentUrl = extra.torrentUrl;
|
if (typeof extra.torrentUrl === "string" && extra.torrentUrl.startsWith("&")) out.torrentUrl = extra.torrentUrl;
|
||||||
if (extra.cipher && typeof extra.cipher === "object" && extra.cipher.key && extra.cipher.manifestBlobId) out.cipher = { v: Number(extra.cipher.v) || 1, key: String(extra.cipher.key), manifestBlobId: String(extra.cipher.manifestBlobId), chunkCount: Number(extra.cipher.chunkCount) || 0 };
|
if (extra.cipher && typeof extra.cipher === "object" && extra.cipher.key && extra.cipher.manifestBlobId) out.cipher = { v: Number(extra.cipher.v) || 1, key: String(extra.cipher.key), manifestBlobId: String(extra.cipher.manifestBlobId), chunkCount: Number(extra.cipher.chunkCount) || 0 };
|
||||||
|
|
@ -36,7 +38,7 @@ const parseBlobId = (blobMarkdown) => {
|
||||||
const voteSum = (opinions = {}) =>
|
const voteSum = (opinions = {}) =>
|
||||||
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
|
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
|
|
@ -44,9 +46,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
return ssb;
|
return ssb;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null;
|
const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null;
|
||||||
const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c;
|
const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c;
|
||||||
const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c;
|
const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c;
|
||||||
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
||||||
const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {};
|
const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {};
|
||||||
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'file') : msgs;
|
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'file') : msgs;
|
||||||
|
|
@ -141,6 +143,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
size: c.size || 0,
|
size: c.size || 0,
|
||||||
mime: undec ? "" : (c.mime || ""),
|
mime: undec ? "" : (c.mime || ""),
|
||||||
fileName: undec ? "" : (c.fileName || ""),
|
fileName: undec ? "" : (c.fileName || ""),
|
||||||
|
license: undec ? "" : (c.license || ""),
|
||||||
torrentKey: undec ? "" : (typeof c.torrentKey === "string" && c.torrentKey.startsWith("%") ? c.torrentKey : ""),
|
torrentKey: undec ? "" : (typeof c.torrentKey === "string" && c.torrentKey.startsWith("%") ? c.torrentKey : ""),
|
||||||
torrentUrl: undec ? "" : (typeof c.torrentUrl === "string" && c.torrentUrl.startsWith("&") ? c.torrentUrl : ""),
|
torrentUrl: undec ? "" : (typeof c.torrentUrl === "string" && c.torrentUrl.startsWith("&") ? c.torrentUrl : ""),
|
||||||
cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null),
|
cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null),
|
||||||
|
|
@ -199,7 +202,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
...fileExtra(extra)
|
...fileExtra(extra)
|
||||||
};
|
};
|
||||||
|
|
||||||
content = await encryptIfTribe(content);
|
content = await encryptIfGroup(content);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res)));
|
ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res)));
|
||||||
|
|
@ -236,12 +239,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const oldMsg = await getMsg(ssbClient, tipId);
|
const oldMsg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!oldMsg) throw new Error("File not found");
|
if (!oldMsg) throw new Error("File not found");
|
||||||
const oldDec = await decryptIfTribe(oldMsg.content);
|
const oldDec = await decryptIfGroup(oldMsg.content);
|
||||||
if (!oldDec || oldDec.type !== "file") throw new Error("File not found");
|
if (!oldDec || oldDec.type !== "file") throw new Error("File not found");
|
||||||
assertReadable(oldDec, "File");
|
assertReadable(oldDec, "File");
|
||||||
if (oldMsg.author !== userId) throw new Error("Not the author");
|
if (oldMsg.author !== userId) throw new Error("Not the author");
|
||||||
const aggFile = await this.getFileById(id, userId);
|
const aggFile = await this.getFileById(id, userId);
|
||||||
if (aggFile && Object.keys(aggFile.opinions || {}).some(k => (aggFile.opinions[k] || 0) > 0)) throw new Error("Cannot edit file after it has received opinions.");
|
opinionRules.assertMediaKept(aggFile, !!blobMarkdown);
|
||||||
|
|
||||||
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags);
|
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags);
|
||||||
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
||||||
|
|
@ -255,7 +258,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
title: title !== undefined ? title || "" : oldDec.title || "",
|
title: title !== undefined ? title || "" : oldDec.title || "",
|
||||||
description: description !== undefined ? description || "" : oldDec.description || "",
|
description: description !== undefined ? description || "" : oldDec.description || "",
|
||||||
size: extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0),
|
size: extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0),
|
||||||
...(extra ? fileExtra(extra) : fileExtra(oldDec)),
|
...fileExtra(blobId ? { license: oldDec.license, ...(extra || {}) } : { ...oldDec, ...(extra || {}) }),
|
||||||
opinions: oldDec.opinions || {},
|
opinions: oldDec.opinions || {},
|
||||||
opinions_inhabitants: oldDec.opinions_inhabitants || [],
|
opinions_inhabitants: oldDec.opinions_inhabitants || [],
|
||||||
author: oldDec.author || userId,
|
author: oldDec.author || userId,
|
||||||
|
|
@ -264,7 +267,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
};
|
};
|
||||||
|
|
||||||
updated = await encryptIfTribe(updated);
|
updated = await encryptIfGroup(updated);
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res)));
|
ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res)));
|
||||||
|
|
@ -281,7 +284,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tipId);
|
const msg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!msg) throw new Error("File not found");
|
if (!msg) throw new Error("File not found");
|
||||||
const dec = await decryptIfTribe(msg.content);
|
const dec = await decryptIfGroup(msg.content);
|
||||||
if (!dec || dec.type !== "file") throw new Error("File not found");
|
if (!dec || dec.type !== "file") throw new Error("File not found");
|
||||||
if (msg.author !== userId) throw new Error("Not the author");
|
if (msg.author !== userId) throw new Error("Not the author");
|
||||||
|
|
||||||
|
|
@ -360,8 +363,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tip);
|
const msg = await getMsg(ssbClient, tip);
|
||||||
if (!msg) throw new Error("File not found");
|
if (!msg) throw new Error("File not found");
|
||||||
let c = msg.content;
|
let c = msg.content;
|
||||||
if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) {
|
if (cryptoModel && (c?.encryptedPayload || cryptoModel.isSealedMsg(c)) && tribesModel) {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel);
|
||||||
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
||||||
}
|
}
|
||||||
if (!c || c.type !== "file") throw new Error("File not found");
|
if (!c || c.type !== "file") throw new Error("File not found");
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,18 @@
|
||||||
// Ver docs/FRONTERA_UPSTREAM.md.
|
// Ver docs/FRONTERA_UPSTREAM.md.
|
||||||
//
|
//
|
||||||
// En el movil los workflows son los skins: cada uno enciende unos modulos y trae su
|
// En el movil los workflows son los skins: cada uno enciende unos modulos y trae su
|
||||||
// barra de abajo. Van de menos a mas: Activista (accion directa, lo minimo),
|
// barra de abajo. Van de menos a mas: Minimo, Acampada, Activista y despues los del
|
||||||
// Acampada, y despues los del dev por numero de modulos.
|
// dev por numero de modulos.
|
||||||
|
|
||||||
const wf = require('./workflows_model');
|
const wf = require('./workflows_model');
|
||||||
|
|
||||||
const FORK_WORKFLOWS = [
|
const FORK_WORKFLOWS = [
|
||||||
{
|
{
|
||||||
// accion directa: convocar, coordinarse, llamar y avisar. Lo minimo para salir a
|
// lo minimo para salir a la calle: convocar, coordinarse, llamar y avisar
|
||||||
// la calle; lo demas se enciende con otro skin
|
// (sin mapas: backend.js los apaga a la fuerza en el movil)
|
||||||
key: 'activista',
|
key: 'minimal',
|
||||||
theme: 'Activista',
|
theme: 'Activista',
|
||||||
homePage: 'activity',
|
homePage: 'activity',
|
||||||
// (sin mapas: backend.js los apaga a la fuerza en el movil)
|
|
||||||
modules: ['chats', 'phone', 'events', 'emergencies', 'invites', 'backup']
|
modules: ['chats', 'phone', 'events', 'emergencies', 'invites', 'backup']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -31,13 +30,27 @@ const FORK_WORKFLOWS = [
|
||||||
homePage: 'activity',
|
homePage: 'activity',
|
||||||
modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
||||||
'tribes', 'parliament', 'votes', 'events']
|
'tribes', 'parliament', 'votes', 'events']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// un colectivo que se organiza: lo de Acampada mas campañas, alertas, encuestas,
|
||||||
|
// calendario, tareas, informes y documentos. Menos que el activists del dev (45)
|
||||||
|
key: 'activista',
|
||||||
|
theme: 'Activista',
|
||||||
|
homePage: 'activity',
|
||||||
|
modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
||||||
|
'tribes', 'parliament', 'votes', 'events', 'emergencies', 'campaigns', 'polls', 'calendars', 'tasks', 'reports',
|
||||||
|
'docs', 'mailing']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// orden en el movil: estos tres primero y el resto por numero de modulos
|
||||||
|
const LADDER = ['minimal', 'acampada', 'activista'];
|
||||||
|
|
||||||
// barra de abajo de cada skin: los cuatro atajos de lo que mas se usa en el (claves
|
// barra de abajo de cada skin: los cuatro atajos de lo que mas se usa en el (claves
|
||||||
// de PINNABLE_MODULES en hive_views.js). Quien la cambia, la cambia para ese skin
|
// de PINNABLE_MODULES en hive_views.js). Quien la cambia, la cambia para ese skin
|
||||||
const BARS = {
|
const BARS = {
|
||||||
activista: ['activity', 'chats', 'events', 'phone'],
|
minimal: ['activity', 'chats', 'events', 'phone'],
|
||||||
|
activista: ['activity', 'tribes', 'chats', 'votes'],
|
||||||
acampada: ['activity', 'chats', 'tribes', 'votes'],
|
acampada: ['activity', 'chats', 'tribes', 'votes'],
|
||||||
activists: ['activity', 'tribes', 'votes', 'forum'],
|
activists: ['activity', 'tribes', 'votes', 'forum'],
|
||||||
social: ['activity', 'feed', 'images', 'chats'],
|
social: ['activity', 'feed', 'images', 'chats'],
|
||||||
|
|
@ -89,4 +102,4 @@ const saveBar = (cfg, pins) => {
|
||||||
return cfg;
|
return cfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { FORK_WORKFLOWS, BARS, register, moduleCount, barFor, saveBar };
|
module.exports = { FORK_WORKFLOWS, LADDER, BARS, register, moduleCount, barFor, saveBar };
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ const { readTyped, requestScope, memoIndex } = require('./typed_log');
|
||||||
const longText = require('../backend/long_text');
|
const longText = require('../backend/long_text');
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
module.exports = ({ cooler, cryptoModel, forumCrypto }) => {
|
||||||
let ssb, userId;
|
let ssb, userId;
|
||||||
const ownCrypto = forumCrypto || tribeCrypto;
|
const ownCrypto = forumCrypto || cryptoModel;
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null;
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null;
|
||||||
|
|
||||||
const decryptForumContent = (rawContent, rootId) => {
|
const decryptForumContent = (rawContent, rootId) => {
|
||||||
if (!rawContent) return rawContent;
|
if (!rawContent) return rawContent;
|
||||||
if (!rawContent.encryptedPayload) return rawContent;
|
if (!rawContent.encryptedPayload) return rawContent;
|
||||||
if (!ownCrypto || !tribeCrypto) return { ...rawContent, _undecryptable: true };
|
if (!ownCrypto || !cryptoModel) return { ...rawContent, _undecryptable: true };
|
||||||
let keys = (rootId && ownCrypto.getKeys && ownCrypto.getKeys(rootId)) || [];
|
let keys = (rootId && ownCrypto.getKeys && ownCrypto.getKeys(rootId)) || [];
|
||||||
if (!keys.length && typeof ownCrypto.getAllRootIds === 'function') {
|
if (!keys.length && typeof ownCrypto.getAllRootIds === 'function') {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
@ -24,7 +24,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!keys.length) return { ...rawContent, _undecryptable: true };
|
if (!keys.length) return { ...rawContent, _undecryptable: true };
|
||||||
const dec = tribeCrypto.decryptContent(rawContent, keys.map(k => [k]));
|
const dec = cryptoModel.decryptContent(rawContent, keys.map(k => [k]));
|
||||||
if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true };
|
if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true };
|
||||||
return { ...dec, _decrypted: true };
|
return { ...dec, _decrypted: true };
|
||||||
};
|
};
|
||||||
|
|
@ -289,8 +289,8 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
isPrivate: isPrivateFlag
|
isPrivate: isPrivateFlag
|
||||||
};
|
};
|
||||||
let forumKey = null;
|
let forumKey = null;
|
||||||
if (isPrivateFlag && ownCrypto && tribeCrypto) forumKey = ownCrypto.generateTribeKey();
|
if (isPrivateFlag && ownCrypto && cryptoModel) forumKey = ownCrypto.generateKey();
|
||||||
const seal = (c) => forumKey ? tribeCrypto.encryptContent(c, [forumKey], true) : c;
|
const seal = (c) => forumKey ? cryptoModel.encryptContent(c, [forumKey], true) : c;
|
||||||
const publish = (c) => new Promise((resolve, reject) =>
|
const publish = (c) => new Promise((resolve, reject) =>
|
||||||
ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res))
|
ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res))
|
||||||
);
|
);
|
||||||
|
|
@ -300,7 +300,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
ownCrypto.setKey(result.key, forumKey, 1);
|
ownCrypto.setKey(result.key, forumKey, 1);
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require('../server/node_modules/ssb-keys');
|
const ssbKeys = require('../server/node_modules/ssb-keys');
|
||||||
const memberKeys = { [userId]: tribeCrypto.boxKeyForMember(forumKey, userId, ssbKeys) };
|
const memberKeys = { [userId]: cryptoModel.boxKeyForMember(forumKey, userId, ssbKeys) };
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: 'tribe-keys', tribeId: result.key, generation: 1, memberKeys }, () => resolve());
|
ssbClient.publish({ type: 'tribe-keys', tribeId: result.key, generation: 1, memberKeys }, () => resolve());
|
||||||
});
|
});
|
||||||
|
|
@ -328,11 +328,11 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
};
|
};
|
||||||
if (parentId) content.branch = parentId;
|
if (parentId) content.branch = parentId;
|
||||||
let key = null;
|
let key = null;
|
||||||
if (isPrivate && ownCrypto && tribeCrypto) {
|
if (isPrivate && ownCrypto && cryptoModel) {
|
||||||
key = lookupKey(forumId);
|
key = lookupKey(forumId);
|
||||||
if (!key) throw new Error('Missing forum key — cannot reply to encrypted forum');
|
if (!key) throw new Error('Missing forum key — cannot reply to encrypted forum');
|
||||||
}
|
}
|
||||||
const seal = (c) => key ? tribeCrypto.encryptContent(c, [key], true) : c;
|
const seal = (c) => key ? cryptoModel.encryptContent(c, [key], true) : c;
|
||||||
const publish = (c) => new Promise((resolve, reject) =>
|
const publish = (c) => new Promise((resolve, reject) =>
|
||||||
ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res))
|
ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res))
|
||||||
);
|
);
|
||||||
|
|
@ -341,7 +341,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
generateInvite: async (forumId) => {
|
generateInvite: async (forumId) => {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable');
|
if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable');
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m)));
|
const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m)));
|
||||||
if (!rawRoot || !rawRoot.content) throw new Error('Forum not found');
|
if (!rawRoot || !rawRoot.content) throw new Error('Forum not found');
|
||||||
|
|
@ -353,10 +353,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
const key = lookupKey(forumId);
|
const key = lookupKey(forumId);
|
||||||
if (!key) throw new Error('Missing forum key');
|
if (!key) throw new Error('Missing forum key');
|
||||||
const code = crypto.randomBytes(16).toString('hex');
|
const code = crypto.randomBytes(16).toString('hex');
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt);
|
const ek = cryptoModel.encryptForInvite(key, code, inviteSalt);
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve());
|
ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve());
|
||||||
});
|
});
|
||||||
return { code, forumId };
|
return { code, forumId };
|
||||||
},
|
},
|
||||||
|
|
@ -367,7 +367,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
generateOpenInvite: async (forumId) => {
|
generateOpenInvite: async (forumId) => {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable');
|
if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable');
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m)));
|
const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m)));
|
||||||
if (!rawRoot || !rawRoot.content) throw new Error('Forum not found');
|
if (!rawRoot || !rawRoot.content) throw new Error('Forum not found');
|
||||||
|
|
@ -380,10 +380,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
const key = lookupKey(forumId);
|
const key = lookupKey(forumId);
|
||||||
if (!key) throw new Error('Missing forum key');
|
if (!key) throw new Error('Missing forum key');
|
||||||
const code = crypto.randomBytes(16).toString('hex');
|
const code = crypto.randomBytes(16).toString('hex');
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt);
|
const ek = cryptoModel.encryptForInvite(key, code, inviteSalt);
|
||||||
const invitePub = await new Promise((resolve, reject) => {
|
const invitePub = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r));
|
ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r));
|
||||||
});
|
});
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish({ type: 'forum-open-invite', v: 1, target: forumId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve());
|
ssbClient.publish({ type: 'forum-open-invite', v: 1, target: forumId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve());
|
||||||
|
|
@ -406,7 +406,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
joinByInvite: async (code) => {
|
joinByInvite: async (code) => {
|
||||||
if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable');
|
if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable');
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const messages = await readForumLog();
|
const messages = await readForumLog();
|
||||||
const authorOf = authorsByKey(messages);
|
const authorOf = authorsByKey(messages);
|
||||||
|
|
@ -418,12 +418,12 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
if (invTomb.has(m.key)) continue;
|
if (invTomb.has(m.key)) continue;
|
||||||
if (!authorOf.get(c.target) || authorOf.get(c.target) !== m.value.author) continue;
|
if (!authorOf.get(c.target) || authorOf.get(c.target) !== m.value.author) continue;
|
||||||
try {
|
try {
|
||||||
const hash = tribeCrypto.hashInviteCode(code, c.salt);
|
const hash = cryptoModel.hashInviteCode(code, c.salt);
|
||||||
if (hash === c.codeHash) { matched = c; break; }
|
if (hash === c.codeHash) { matched = c; break; }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!matched) throw new Error('Invalid or expired invite code');
|
if (!matched) throw new Error('Invalid or expired invite code');
|
||||||
const forumKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt);
|
const forumKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt);
|
||||||
if (!forumKey) throw new Error('Could not decrypt invite');
|
if (!forumKey) throw new Error('Could not decrypt invite');
|
||||||
ownCrypto.addNewKey(matched.target, forumKey);
|
ownCrypto.addNewKey(matched.target, forumKey);
|
||||||
return { ok: true, forumId: matched.target };
|
return { ok: true, forumId: matched.target };
|
||||||
|
|
@ -521,7 +521,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getMessagesByForumId: async forumId => {
|
getMessagesByForumId: async (forumId, ssbReplies = []) => {
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const msgs = await readForumLog();
|
const msgs = await readForumLog();
|
||||||
const deleted = deletedOf(msgs);
|
const deleted = deletedOf(msgs);
|
||||||
|
|
@ -537,8 +537,11 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
||||||
if (c.root !== forumId) return null;
|
if (c.root !== forumId) return null;
|
||||||
return { c, m };
|
return { c, m };
|
||||||
};
|
};
|
||||||
const replies = msgs
|
const parentOf = (branch) => [].concat(branch || []).filter(b => typeof b === 'string' && b !== forumId).pop() || null;
|
||||||
.map(decodeReply)
|
const fromPosts = (Array.isArray(ssbReplies) ? ssbReplies : [])
|
||||||
|
.filter(m => m && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.root === forumId)
|
||||||
|
.map(m => ({ c: { ...m.value.content, branch: parentOf(m.value.content.branch) }, m }));
|
||||||
|
const replies = [...msgs.map(decodeReply), ...fromPosts]
|
||||||
.filter(r => r && !deleted.has(r.m.key))
|
.filter(r => r && !deleted.has(r.m.key))
|
||||||
.map(({ c, m }) => ({
|
.map(({ c, m }) => ({
|
||||||
key: m.key,
|
key: m.key,
|
||||||
|
|
|
||||||
|
|
@ -68,47 +68,47 @@ const matchSearch = (item, q) => {
|
||||||
return hay.includes(qq)
|
return hay.includes(qq)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto }) => {
|
module.exports = ({ cooler, cryptoModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
const isEncrypted = (c) => !!(c && c.encryptedPayload)
|
const isEncrypted = (c) => !!(c && c.encryptedPayload)
|
||||||
|
|
||||||
const keysForRoot = (rootId) => {
|
const keysForRoot = (rootId) => {
|
||||||
if (!tribeCrypto || !rootId) return []
|
if (!cryptoModel || !rootId) return []
|
||||||
const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || []
|
const ks = (cryptoModel.getKeys && cryptoModel.getKeys(rootId)) || []
|
||||||
if (ks.length) return ks
|
if (ks.length) return ks
|
||||||
const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null
|
const k = cryptoModel.getKey ? cryptoModel.getKey(rootId) : null
|
||||||
return k ? [k] : []
|
return k ? [k] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptHousing = (c, rootId) => {
|
const decryptHousing = (c, rootId) => {
|
||||||
if (!isEncrypted(c)) return c
|
if (!isEncrypted(c)) return c
|
||||||
if (!tribeCrypto) return { ...c, _undecryptable: true }
|
if (!cryptoModel) return { ...c, _undecryptable: true }
|
||||||
const keys = keysForRoot(rootId)
|
const keys = keysForRoot(rootId)
|
||||||
if (!keys.length) return { ...c, _undecryptable: true }
|
if (!keys.length) return { ...c, _undecryptable: true }
|
||||||
return tribeCrypto.decryptContent(c, keys.map(k => [k]))
|
return cryptoModel.decryptContent(c, keys.map(k => [k]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => {
|
const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => {
|
||||||
const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN"
|
const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN"
|
||||||
if (!hidden || !tribeCrypto) {
|
if (!hidden || !cryptoModel) {
|
||||||
ssbClient.publish(content, (err, msg) => {
|
ssbClient.publish(content, (err, msg) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
if (msg && msg.key && tribeCrypto && !rootId) {
|
if (msg && msg.key && cryptoModel && !rootId) {
|
||||||
try { tribeCrypto.setKey(msg.key, tribeCrypto.generateTribeKey(), 1) } catch (_) {}
|
try { cryptoModel.setKey(msg.key, cryptoModel.generateKey(), 1) } catch (_) {}
|
||||||
}
|
}
|
||||||
resolve(msg)
|
resolve(msg)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let key = rootId ? (keysForRoot(rootId)[0] || null) : null
|
let key = rootId ? (keysForRoot(rootId)[0] || null) : null
|
||||||
if (!key) key = tribeCrypto.generateTribeKey()
|
if (!key) key = cryptoModel.generateKey()
|
||||||
let envelope
|
let envelope
|
||||||
try { envelope = tribeCrypto.encryptContent(content, [key], true) } catch (err) { return reject(err) }
|
try { envelope = cryptoModel.encryptContent(content, [key], true) } catch (err) { return reject(err) }
|
||||||
ssbClient.publish(envelope, (err, msg) => {
|
ssbClient.publish(envelope, (err, msg) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
try { tribeCrypto.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {}
|
try { cryptoModel.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {}
|
||||||
resolve(msg)
|
resolve(msg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const categories = require("../backend/opinion_categories");
|
const categories = require("../backend/opinion_categories");
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
|
|
@ -170,7 +171,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (!oldMsg || oldMsg.content?.type !== "image") throw new Error("Image not found");
|
if (!oldMsg || oldMsg.content?.type !== "image") throw new Error("Image not found");
|
||||||
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
||||||
const aggImg = await this.getImageById(id, userId);
|
const aggImg = await this.getImageById(id, userId);
|
||||||
if (aggImg && Object.keys(aggImg.opinions || {}).some(k => (aggImg.opinions[k] || 0) > 0)) throw new Error("Cannot edit image after it has received opinions.");
|
opinionRules.assertMediaKept(aggImg, !!blobMarkdown);
|
||||||
|
|
||||||
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
||||||
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
||||||
|
|
|
||||||
|
|
@ -724,6 +724,7 @@ module.exports = ({ cooler }) => {
|
||||||
builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0))
|
builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0))
|
||||||
return {
|
return {
|
||||||
...b.blueprint,
|
...b.blueprint,
|
||||||
|
rootId: b.root,
|
||||||
facilityId: g.root,
|
facilityId: g.root,
|
||||||
facilityName: g.facility.name,
|
facilityName: g.facility.name,
|
||||||
members: g.facility.members,
|
members: g.facility.members,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const matchSearch = (job, q) => {
|
||||||
return hay.includes(qq)
|
return hay.includes(qq)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto }) => {
|
module.exports = ({ cooler, cryptoModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildJobObject = (node, rootId, subscribers) => {
|
const buildJobObject = (node, rootId, subscribers) => {
|
||||||
const visibleSubs = (tribeCrypto && tribeCrypto.getKey(rootId)) || ssb?.id === (node.author || node.c?.author) ? subscribers : [];
|
const visibleSubs = (cryptoModel && cryptoModel.getKey(rootId)) || ssb?.id === (node.author || node.c?.author) ? subscribers : [];
|
||||||
const c = node.c || {}
|
const c = node.c || {}
|
||||||
let blobId = c.image || null
|
let blobId = c.image || null
|
||||||
if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1]
|
if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1]
|
||||||
|
|
@ -207,9 +207,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
|
|
||||||
return new Promise((res, rej) => ssbClient.publish(content, (e, m) => {
|
return new Promise((res, rej) => ssbClient.publish(content, (e, m) => {
|
||||||
if (e) return rej(e)
|
if (e) return rej(e)
|
||||||
if (m && m.key && tribeCrypto) {
|
if (m && m.key && cryptoModel) {
|
||||||
const key = tribeCrypto.generateTribeKey()
|
const key = cryptoModel.generateKey()
|
||||||
tribeCrypto.setKey(m.key, key, 1)
|
cryptoModel.setKey(m.key, key, 1)
|
||||||
}
|
}
|
||||||
res(m)
|
res(m)
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
const { readTyped, CONTENT_TYPES } = require("./typed_log");
|
const { readTyped, CONTENT_TYPES } = require("./typed_log");
|
||||||
|
const { isComment } = require("./comments_model");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
|
|
||||||
|
|
@ -63,6 +64,7 @@ function idsEqual(a, b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeType(a) {
|
function normalizeType(a) {
|
||||||
|
if (isComment(a.content)) return "comment";
|
||||||
const t = a.type || a.content?.type || inferType(a.content) || "";
|
const t = a.type || a.content?.type || inferType(a.content) || "";
|
||||||
return String(t).toLowerCase();
|
return String(t).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ function getGoverningPeriodId(now = new Date()) {
|
||||||
return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`;
|
return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
module.exports = ({ cooler, tribesModel, cryptoModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
@ -204,6 +204,11 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||||
return candidates[0];
|
return candidates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function houseTribeReady(houseKey) {
|
||||||
|
if (await findMyHouseTribe(houseKey).catch(() => null)) return true;
|
||||||
|
return houseKey === 'academia' && !!(await findAnyHouseTribe(houseKey).catch(() => null));
|
||||||
|
}
|
||||||
|
|
||||||
async function findAnyHouseTribe(houseKey) {
|
async function findAnyHouseTribe(houseKey) {
|
||||||
if (!tribesModel || !VALID_KEY(houseKey)) return null;
|
if (!tribesModel || !VALID_KEY(houseKey)) return null;
|
||||||
let list = [];
|
let list = [];
|
||||||
|
|
@ -307,8 +312,8 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||||
if (houseKey !== 'academia') {
|
if (houseKey !== 'academia') {
|
||||||
await publishHouseAnchorTombstone(houseKey, rootId).catch(() => {});
|
await publishHouseAnchorTombstone(houseKey, rootId).catch(() => {});
|
||||||
}
|
}
|
||||||
if (tribeCrypto && typeof tribeCrypto.dropKey === 'function') {
|
if (cryptoModel && typeof cryptoModel.dropKey === 'function') {
|
||||||
try { tribeCrypto.dropKey(rootId); } catch (_) {}
|
try { cryptoModel.dropKey(rootId); } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,7 +392,10 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||||
return myCanonical.tribe;
|
return myCanonical.tribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchor) return null;
|
if (anchor) {
|
||||||
|
await redeemPendingAutoInvites().catch(() => {});
|
||||||
|
return await findMyHouseTribe(houseKey);
|
||||||
|
}
|
||||||
|
|
||||||
const existingAny = await findAnyHouseTribe(houseKey).catch(() => null);
|
const existingAny = await findAnyHouseTribe(houseKey).catch(() => null);
|
||||||
if (existingAny) {
|
if (existingAny) {
|
||||||
|
|
@ -437,8 +445,8 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||||
if (houseKey !== 'academia') {
|
if (houseKey !== 'academia') {
|
||||||
await publishHouseAnchorTombstone(houseKey, t.rootId).catch(() => {});
|
await publishHouseAnchorTombstone(houseKey, t.rootId).catch(() => {});
|
||||||
}
|
}
|
||||||
if (tribeCrypto && typeof tribeCrypto.dropKey === 'function') {
|
if (cryptoModel && typeof cryptoModel.dropKey === 'function') {
|
||||||
try { tribeCrypto.dropKey(t.rootId); } catch (_) {}
|
try { cryptoModel.dropKey(t.rootId); } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -954,6 +962,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||||
createHouseInvite,
|
createHouseInvite,
|
||||||
redeemHouseInvite,
|
redeemHouseInvite,
|
||||||
findMyHouseTribe,
|
findMyHouseTribe,
|
||||||
|
houseTribeReady,
|
||||||
ensureHouseTribe,
|
ensureHouseTribe,
|
||||||
leaveMyHouseTribe,
|
leaveMyHouseTribe,
|
||||||
issueAutoInvitesForMyHouse,
|
issueAutoInvitesForMyHouse,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { isComment } = require('./comments_model');
|
||||||
const pull = require('../server/node_modules/pull-stream');
|
const pull = require('../server/node_modules/pull-stream');
|
||||||
const { requestScope } = require('./typed_log');
|
const { requestScope } = require('./typed_log');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
|
@ -60,6 +61,7 @@ const ACTION_TYPES = new Set([
|
||||||
|
|
||||||
const ACTION_PHRASES = {
|
const ACTION_PHRASES = {
|
||||||
post: 'published a post',
|
post: 'published a post',
|
||||||
|
comment: 'commented on some content',
|
||||||
about: 'updated profile information',
|
about: 'updated profile information',
|
||||||
contact: 'followed or unfollowed someone',
|
contact: 'followed or unfollowed someone',
|
||||||
feed: 'shared content in the feed',
|
feed: 'shared content in the feed',
|
||||||
|
|
@ -243,7 +245,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (!ACTION_TYPES.has(c.type)) continue;
|
if (!ACTION_TYPES.has(c.type)) continue;
|
||||||
const ts = v.timestamp || 0;
|
const ts = v.timestamp || 0;
|
||||||
const summary = c.title || c.text || c.question || c.subject || c.name || c.concept || c.description || '';
|
const summary = c.title || c.text || c.question || c.subject || c.name || c.concept || c.description || '';
|
||||||
out.push({ key: m.key, ts, type: c.type, summary: compact(summary) });
|
out.push({ key: m.key, ts, type: isComment(c) ? 'comment' : c.type, summary: compact(summary) });
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ models.about = {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [id, p] of out) if (p.name) nameCache.set(id, p.name, now);
|
for (const [id, p] of out) { if (p.name) nameCache.set(id, p.name, now); if (p.image) nameCache.setImage(id, p.image); }
|
||||||
return out;
|
return out;
|
||||||
})();
|
})();
|
||||||
if (store) store.aboutProfiles = pending;
|
if (store) store.aboutProfiles = pending;
|
||||||
|
|
@ -433,6 +433,7 @@ models.about = {
|
||||||
return nullImage;
|
return nullImage;
|
||||||
}
|
}
|
||||||
if (typeof raw.link === "string") {
|
if (typeof raw.link === "string") {
|
||||||
|
nameCache.setImage(feedId, raw.link);
|
||||||
return raw.link;
|
return raw.link;
|
||||||
}
|
}
|
||||||
return raw;
|
return raw;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const normalizeTags = (raw) => {
|
||||||
|
|
||||||
const ALLOWED_MAP_TYPES = new Set(["OPEN", "CLOSED", "SINGLE"]);
|
const ALLOWED_MAP_TYPES = new Set(["OPEN", "CLOSED", "SINGLE"]);
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, mapCrypto, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
|
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
|
|
@ -27,16 +27,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
return ssb;
|
return ssb;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ownCrypto = mapCrypto || tribeCrypto;
|
const ownCrypto = mapCrypto || cryptoModel;
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null;
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null;
|
||||||
const lookupKeys = (rid) => {
|
const lookupKeys = (rid) => {
|
||||||
const a = (ownCrypto && ownCrypto.getKeys(rid)) || [];
|
const a = (ownCrypto && ownCrypto.getKeys(rid)) || [];
|
||||||
if (a.length) return a;
|
if (a.length) return a;
|
||||||
return (tribeCrypto && tribeCrypto.getKeys(rid)) || [];
|
return (cryptoModel && cryptoModel.getKeys(rid)) || [];
|
||||||
};
|
};
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0);
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0);
|
||||||
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex"); } catch (_) { return null; } };
|
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex"); } catch (_) { return null; } };
|
||||||
const hashCode = (code, salt) => (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null);
|
const hashCode = (code, salt) => (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null);
|
||||||
const inviteMatches = (inv, code) => {
|
const inviteMatches = (inv, code) => {
|
||||||
if (!code) return false;
|
if (!code) return false;
|
||||||
if (typeof inv === "string") return inv === code;
|
if (typeof inv === "string") return inv === code;
|
||||||
|
|
@ -47,16 +47,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "");
|
const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "");
|
||||||
|
|
||||||
const rotateMapKey = async (rootId, remainingMembers) => {
|
const rotateMapKey = async (rootId, remainingMembers) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rootId) return;
|
if (!ownCrypto || !cryptoModel || !rootId) return;
|
||||||
const existing = lookupKey(rootId);
|
const existing = lookupKey(rootId);
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
const newKey = ownCrypto.generateTribeKey();
|
const newKey = ownCrypto.generateKey();
|
||||||
const newGen = ownCrypto.addNewKey(rootId, newKey);
|
const newGen = ownCrypto.addNewKey(rootId, newKey);
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -91,34 +91,34 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null;
|
const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null;
|
||||||
const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c;
|
const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c;
|
||||||
const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c;
|
const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c;
|
||||||
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
||||||
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, ['map', 'mapMarker']) : msgs;
|
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, ['map', 'mapMarker']) : msgs;
|
||||||
const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author };
|
const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author };
|
||||||
|
|
||||||
const encryptStandalone = (content, rootId) => {
|
const encryptStandalone = (content, rootId) => {
|
||||||
if (!tribeCrypto || !rootId) return content;
|
if (!cryptoModel || !rootId) return content;
|
||||||
const key = lookupKey(rootId);
|
const key = lookupKey(rootId);
|
||||||
if (!key) return content;
|
if (!key) return content;
|
||||||
return tribeCrypto.encryptContent(content, [key], true);
|
return cryptoModel.encryptContent(content, [key], true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryDecryptPublicInviteKey = (invites) => {
|
const tryDecryptPublicInviteKey = (invites) => {
|
||||||
if (!tribeCrypto || !Array.isArray(invites)) return null;
|
if (!cryptoModel || !Array.isArray(invites)) return null;
|
||||||
for (const inv of invites) {
|
for (const inv of invites) {
|
||||||
if (!inv || typeof inv !== "object" || inv.public !== true) continue;
|
if (!inv || typeof inv !== "object" || inv.public !== true) continue;
|
||||||
if (typeof inv.code !== "string") continue;
|
if (typeof inv.code !== "string") continue;
|
||||||
if (typeof inv.ek === "string") {
|
if (typeof inv.ek === "string") {
|
||||||
try {
|
try {
|
||||||
const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt);
|
const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt);
|
||||||
if (k) return k;
|
if (k) return k;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (typeof inv.ekChain === "string") {
|
if (typeof inv.ekChain === "string") {
|
||||||
try {
|
try {
|
||||||
const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt);
|
const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt);
|
||||||
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key;
|
if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -128,24 +128,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
|
|
||||||
const decryptMapRoot = (content, rootId) => {
|
const decryptMapRoot = (content, rootId) => {
|
||||||
if (!content || !content.encryptedPayload) return content;
|
if (!content || !content.encryptedPayload) return content;
|
||||||
if (!tribeCrypto) return content;
|
if (!cryptoModel) return content;
|
||||||
const keys = lookupKeys(rootId);
|
const keys = lookupKeys(rootId);
|
||||||
let candidateChains = (keys || []).map(k => [k]);
|
let candidateChains = (keys || []).map(k => [k]);
|
||||||
const pubKey = tryDecryptPublicInviteKey(content.invites);
|
const pubKey = tryDecryptPublicInviteKey(content.invites);
|
||||||
if (pubKey) candidateChains.push([pubKey]);
|
if (pubKey) candidateChains.push([pubKey]);
|
||||||
if (!candidateChains.length) return { ...content, _undecryptable: true };
|
if (!candidateChains.length) return { ...content, _undecryptable: true };
|
||||||
return tribeCrypto.decryptContent(content, candidateChains);
|
return cryptoModel.decryptContent(content, candidateChains);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptIndexNodes = async (idx) => {
|
const decryptIndexNodes = async (idx) => {
|
||||||
if (!tribeCrypto) return;
|
if (!cryptoModel) return;
|
||||||
for (const [k, n] of (idx.nodes ? idx.nodes.entries() : [])) {
|
for (const [k, n] of (idx.nodes ? idx.nodes.entries() : [])) {
|
||||||
if (!n.c || !n.c.encryptedPayload) continue;
|
if (!n.c || !n.c.encryptedPayload) continue;
|
||||||
const root = idx.rootOf(k);
|
const root = idx.rootOf(k);
|
||||||
let dec = null;
|
let dec = null;
|
||||||
if (n.c.tribeId && tribesModel) {
|
if (n.c.tribeId && tribesModel) {
|
||||||
try {
|
try {
|
||||||
const r = await tribeCrypto.decryptFromTribe(n.c, tribesModel);
|
const r = await cryptoModel.decryptFromGroup(n.c, tribesModel);
|
||||||
if (r && !r._undecryptable) dec = r;
|
if (r && !r._undecryptable) dec = r;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +342,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureMemberKeys = async (ssbClient, messages, items) => {
|
const ensureMemberKeys = async (ssbClient, messages, items) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return;
|
if (!cryptoModel || !ownCrypto) return;
|
||||||
const leavesByRoot = new Map();
|
const leavesByRoot = new Map();
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content;
|
const c = m.value && m.value.content;
|
||||||
|
|
@ -363,7 +363,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(rootId);
|
const key = lookupKey(rootId);
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue;
|
if (!Object.keys(memberKeys).length) continue;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -378,8 +378,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
for (const r of raws) {
|
for (const r of raws) {
|
||||||
if (idx.tomb.has(r.key)) continue;
|
if (idx.tomb.has(r.key)) continue;
|
||||||
let c = r.c;
|
let c = r.c;
|
||||||
if (c.encryptedPayload && tribeCrypto && tribesModel) {
|
if (c.encryptedPayload && cryptoModel && tribesModel) {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel);
|
||||||
if (dec && !dec._undecryptable) c = dec;
|
if (dec && !dec._undecryptable) c = dec;
|
||||||
}
|
}
|
||||||
list.push({
|
list.push({
|
||||||
|
|
@ -530,14 +530,14 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPublicOpen = mType === "OPEN" && !tribeId;
|
const isPublicOpen = mType === "OPEN" && !tribeId;
|
||||||
const shouldEncryptStandalone = !tribeId && !isPublicOpen && tribeCrypto;
|
const shouldEncryptStandalone = !tribeId && !isPublicOpen && cryptoModel;
|
||||||
let mapKey = null;
|
let mapKey = null;
|
||||||
let content = plainContent;
|
let content = plainContent;
|
||||||
if (tribeId) {
|
if (tribeId) {
|
||||||
content = await encryptIfTribe(plainContent);
|
content = await encryptIfGroup(plainContent);
|
||||||
} else if (shouldEncryptStandalone) {
|
} else if (shouldEncryptStandalone) {
|
||||||
mapKey = ownCrypto.generateTribeKey();
|
mapKey = ownCrypto.generateKey();
|
||||||
content = tribeCrypto.encryptContent(plainContent, [mapKey], true);
|
content = cryptoModel.encryptContent(plainContent, [mapKey], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
|
@ -548,7 +548,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
ownCrypto.setKey(result.key, mapKey, 1);
|
ownCrypto.setKey(result.key, mapKey, 1);
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const boxedKey = tribeCrypto.boxKeyForMember(mapKey, userId, ssbKeys);
|
const boxedKey = cryptoModel.boxKeyForMember(mapKey, userId, ssbKeys);
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve());
|
ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve());
|
||||||
});
|
});
|
||||||
|
|
@ -556,8 +556,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
if (mType === "OPEN") {
|
if (mType === "OPEN") {
|
||||||
try {
|
try {
|
||||||
const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex");
|
const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex");
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ek = tribeCrypto.encryptForInvite(mapKey, pubCode, inviteSalt);
|
const ek = cryptoModel.encryptForInvite(mapKey, pubCode, inviteSalt);
|
||||||
let updated = {
|
let updated = {
|
||||||
type: "map",
|
type: "map",
|
||||||
replaces: result.key,
|
replaces: result.key,
|
||||||
|
|
@ -593,10 +593,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const oldMsg = await getMsg(ssbClient, tipId);
|
const oldMsg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!oldMsg) throw new Error("Map not found");
|
if (!oldMsg) throw new Error("Map not found");
|
||||||
const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content);
|
const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content);
|
||||||
const tribeIdHint = isWrapped ? null : (oldMsg.content && oldMsg.content.tribeId);
|
const tribeIdHint = isWrapped ? null : (oldMsg.content && oldMsg.content.tribeId);
|
||||||
const oldDecrypted = (isWrapped || tribeIdHint)
|
const oldDecrypted = (isWrapped || tribeIdHint)
|
||||||
? await decryptIfTribe(oldMsg.content)
|
? await decryptIfGroup(oldMsg.content)
|
||||||
: decryptMapRoot(oldMsg.content, rootId);
|
: decryptMapRoot(oldMsg.content, rootId);
|
||||||
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
||||||
if (!oldDecrypted || (oldDecrypted.type && oldDecrypted.type !== "map" && !isWrapped && oldMsg.content?.type !== "map")) throw new Error("Map not found");
|
if (!oldDecrypted || (oldDecrypted.type && oldDecrypted.type !== "map" && !isWrapped && oldMsg.content?.type !== "map")) throw new Error("Map not found");
|
||||||
|
|
@ -627,7 +627,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (effectiveTribeId) {
|
if (effectiveTribeId) {
|
||||||
updated = await encryptIfTribe(updated);
|
updated = await encryptIfGroup(updated);
|
||||||
} else if (mType !== "OPEN") {
|
} else if (mType !== "OPEN") {
|
||||||
updated = encryptStandalone(updated, rootId);
|
updated = encryptStandalone(updated, rootId);
|
||||||
}
|
}
|
||||||
|
|
@ -647,7 +647,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tipId);
|
const msg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!msg) throw new Error("Map not found");
|
if (!msg) throw new Error("Map not found");
|
||||||
const decrypted = await decryptIfTribe(msg.content);
|
const decrypted = await decryptIfGroup(msg.content);
|
||||||
if (!decrypted) throw new Error("Map not found");
|
if (!decrypted) throw new Error("Map not found");
|
||||||
if ((decrypted.author || msg.content.author) !== userId) throw new Error("Not the author");
|
if ((decrypted.author || msg.content.author) !== userId) throw new Error("Not the author");
|
||||||
|
|
||||||
|
|
@ -673,7 +673,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const node = idx.nodes.get(tipId);
|
const node = idx.nodes.get(tipId);
|
||||||
if (!node) throw new Error("Map not found");
|
if (!node) throw new Error("Map not found");
|
||||||
|
|
||||||
const mapDecrypted = await decryptIfTribe(node.c);
|
const mapDecrypted = await decryptIfGroup(node.c);
|
||||||
assertReadable(mapDecrypted, "Map");
|
assertReadable(mapDecrypted, "Map");
|
||||||
const mapType = mapDecrypted.mapType || node.c.mapType || "SINGLE";
|
const mapType = mapDecrypted.mapType || node.c.mapType || "SINGLE";
|
||||||
const mapAuthor = node.author;
|
const mapAuthor = node.author;
|
||||||
|
|
@ -694,10 +694,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
if (image) content.image = image;
|
if (image) content.image = image;
|
||||||
|
|
||||||
if (node.c.tribeId) {
|
if (node.c.tribeId) {
|
||||||
content = await encryptIfTribe(content);
|
content = await encryptIfGroup(content);
|
||||||
} else if (tribeCrypto) {
|
} else if (cryptoModel) {
|
||||||
const mapKey = lookupKey(rootId);
|
const mapKey = lookupKey(rootId);
|
||||||
if (mapKey) content = tribeCrypto.encryptContent(content, [mapKey], true);
|
if (mapKey) content = cryptoModel.encryptContent(content, [mapKey], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
@ -711,8 +711,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, markerId);
|
const msg = await getMsg(ssbClient, markerId);
|
||||||
if (!msg || !msg.content) throw new Error("Marker not found");
|
if (!msg || !msg.content) throw new Error("Marker not found");
|
||||||
let c = msg.content;
|
let c = msg.content;
|
||||||
if (c.encryptedPayload && tribeCrypto && tribesModel) {
|
if (c.encryptedPayload && cryptoModel && tribesModel) {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel);
|
||||||
if (dec && !dec._undecryptable) c = dec;
|
if (dec && !dec._undecryptable) c = dec;
|
||||||
}
|
}
|
||||||
if (c.type !== "mapMarker" && !c.encryptedPayload) throw new Error("Marker not found");
|
if (c.type !== "mapMarker" && !c.encryptedPayload) throw new Error("Marker not found");
|
||||||
|
|
@ -774,8 +774,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tip);
|
const msg = await getMsg(ssbClient, tip);
|
||||||
if (!msg || msg.content?.type !== "map") throw new Error("Map not found");
|
if (!msg || msg.content?.type !== "map") throw new Error("Map not found");
|
||||||
let c = msg.content;
|
let c = msg.content;
|
||||||
if (c.encryptedPayload && tribeCrypto && tribesModel) {
|
if (c.encryptedPayload && cryptoModel && tribesModel) {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel);
|
||||||
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
||||||
}
|
}
|
||||||
const markerList = markersFor(idx, tip, root);
|
const markerList = markersFor(idx, tip, root);
|
||||||
|
|
@ -795,21 +795,21 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
if (map.author !== userId) throw new Error("Only the author can generate invites");
|
if (map.author !== userId) throw new Error("Only the author can generate invites");
|
||||||
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex");
|
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex");
|
||||||
let invite = code;
|
let invite = code;
|
||||||
if (tribeCrypto && !map.tribeId) {
|
if (cryptoModel && !map.tribeId) {
|
||||||
const inviteSalt = tribeCrypto.generateInviteSalt();
|
const inviteSalt = cryptoModel.generateInviteSalt();
|
||||||
const ident = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt) };
|
const ident = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt) };
|
||||||
const ekChain = tribeCrypto.encryptChainForInvite([map.rootId || map.key], code, inviteSalt);
|
const ekChain = cryptoModel.encryptChainForInvite([map.rootId || map.key], code, inviteSalt);
|
||||||
const mapKey = ekChain ? null : lookupKey(map.rootId || map.key);
|
const mapKey = ekChain ? null : lookupKey(map.rootId || map.key);
|
||||||
if (ekChain) invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 };
|
if (ekChain) invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 };
|
||||||
else if (mapKey) invite = { ...ident, ek: tribeCrypto.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 };
|
else if (mapKey) invite = { ...ident, ek: cryptoModel.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 };
|
||||||
}
|
}
|
||||||
if (opts.public && typeof invite !== "object") invite = { code, public: true };
|
if (opts.public && typeof invite !== "object") invite = { code, public: true };
|
||||||
const tipId = await this.resolveCurrentId(mapId);
|
const tipId = await this.resolveCurrentId(mapId);
|
||||||
const rootId = await this.resolveRootId(mapId);
|
const rootId = await this.resolveRootId(mapId);
|
||||||
const oldMsg = await getMsg(ssbClient, tipId);
|
const oldMsg = await getMsg(ssbClient, tipId);
|
||||||
const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content);
|
const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content);
|
||||||
const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId))
|
const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId))
|
||||||
? await decryptIfTribe(oldMsg.content)
|
? await decryptIfGroup(oldMsg.content)
|
||||||
: decryptMapRoot(oldMsg.content, rootId);
|
: decryptMapRoot(oldMsg.content, rootId);
|
||||||
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
||||||
const invites = [...(Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []), invite];
|
const invites = [...(Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []), invite];
|
||||||
|
|
@ -831,7 +831,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
createdAt: oldDecrypted.createdAt,
|
createdAt: oldDecrypted.createdAt,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
if (effectiveTribeId) updated = await encryptIfTribe(updated);
|
if (effectiveTribeId) updated = await encryptIfGroup(updated);
|
||||||
else updated = encryptStandalone(updated, rootId);
|
else updated = encryptStandalone(updated, rootId);
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r)));
|
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r)));
|
||||||
const tomb1 = await tombFor(tipId, effectiveTribeId, userId);
|
const tomb1 = await tombFor(tipId, effectiveTribeId, userId);
|
||||||
|
|
@ -865,9 +865,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const tipId = await this.resolveCurrentId(mapId);
|
const tipId = await this.resolveCurrentId(mapId);
|
||||||
const rootId = await this.resolveRootId(mapId);
|
const rootId = await this.resolveRootId(mapId);
|
||||||
const oldMsg = await getMsg(ssbClient, tipId);
|
const oldMsg = await getMsg(ssbClient, tipId);
|
||||||
const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content);
|
const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content);
|
||||||
const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId))
|
const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId))
|
||||||
? await decryptIfTribe(oldMsg.content)
|
? await decryptIfGroup(oldMsg.content)
|
||||||
: decryptMapRoot(oldMsg.content, rootId);
|
: decryptMapRoot(oldMsg.content, rootId);
|
||||||
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId;
|
||||||
const invites = (Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true));
|
const invites = (Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true));
|
||||||
|
|
@ -889,7 +889,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
createdAt: oldDecrypted.createdAt,
|
createdAt: oldDecrypted.createdAt,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
if (effectiveTribeId) updated = await encryptIfTribe(updated);
|
if (effectiveTribeId) updated = await encryptIfGroup(updated);
|
||||||
else updated = encryptStandalone(updated, rootId);
|
else updated = encryptStandalone(updated, rootId);
|
||||||
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r)));
|
await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r)));
|
||||||
const tomb1 = await tombFor(tipId, effectiveTribeId, userId);
|
const tomb1 = await tombFor(tipId, effectiveTribeId, userId);
|
||||||
|
|
@ -920,13 +920,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const token = inviteToken(matchedInvite) || code;
|
const token = inviteToken(matchedInvite) || code;
|
||||||
let mapKey = null;
|
let mapKey = null;
|
||||||
let chain = null;
|
let chain = null;
|
||||||
if (tribeCrypto && typeof matchedInvite === "object") {
|
if (cryptoModel && typeof matchedInvite === "object") {
|
||||||
if (matchedInvite.ekChain) {
|
if (matchedInvite.ekChain) {
|
||||||
chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3);
|
chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3);
|
||||||
if (Array.isArray(chain) && chain.length) mapKey = chain[0].key || (Array.isArray(chain[0].keys) ? chain[0].keys[0] : null);
|
if (Array.isArray(chain) && chain.length) mapKey = chain[0].key || (Array.isArray(chain[0].keys) ? chain[0].keys[0] : null);
|
||||||
else chain = null;
|
else chain = null;
|
||||||
} else if (matchedInvite.ek) {
|
} else if (matchedInvite.ek) {
|
||||||
mapKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt);
|
mapKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isPublic) {
|
if (!isPublic) {
|
||||||
|
|
@ -939,9 +939,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
if (chain) {
|
if (chain) {
|
||||||
for (const entry of chain) {
|
for (const entry of chain) {
|
||||||
if (Array.isArray(entry.keys) && entry.keys.length) {
|
if (Array.isArray(entry.keys) && entry.keys.length) {
|
||||||
tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
||||||
} else if (entry.key) {
|
} else if (entry.key) {
|
||||||
tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1);
|
cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mapKey) {
|
} else if (mapKey) {
|
||||||
|
|
@ -949,13 +949,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
if (!mapKey && matched.contentEncrypted && !matched.tribeId) throw new Error("Invalid or expired invite code");
|
if (!mapKey && matched.contentEncrypted && !matched.tribeId) throw new Error("Invalid or expired invite code");
|
||||||
await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code: token }), ...(mapKey ? { keyProof: keyProofFor(mapKey, userId) } : {}) }, (e, r) => e ? reject(e) : resolve(r)));
|
await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code: token }), ...(mapKey ? { keyProof: keyProofFor(mapKey, userId) } : {}) }, (e, r) => e ? reject(e) : resolve(r)));
|
||||||
if (tribeCrypto && mapKey) {
|
if (cryptoModel && mapKey) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys");
|
const ssbKeys = require("../server/node_modules/ssb-keys");
|
||||||
const memberKeys = {};
|
const memberKeys = {};
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(mapKey, userId, ssbKeys); } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(mapKey, userId, ssbKeys); } catch (_) {}
|
||||||
if (matched.author && matched.author !== userId) {
|
if (matched.author && matched.author !== userId) {
|
||||||
try { memberKeys[matched.author] = tribeCrypto.boxKeyForMember(mapKey, matched.author, ssbKeys); } catch (_) {}
|
try { memberKeys[matched.author] = cryptoModel.boxKeyForMember(mapKey, matched.author, ssbKeys); } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -972,7 +972,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => {
|
||||||
const map = await this.getMapById(mapId, userId);
|
const map = await this.getMapById(mapId, userId);
|
||||||
if (!map) throw new Error("Map not found");
|
if (!map) throw new Error("Map not found");
|
||||||
if (Array.isArray(map.members) && map.members.includes(userId)) return map.rootId || map.key;
|
if (Array.isArray(map.members) && map.members.includes(userId)) return map.rootId || map.key;
|
||||||
if (tribeCrypto && Array.isArray(map.invites)) {
|
if (cryptoModel && Array.isArray(map.invites)) {
|
||||||
const pub = map.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain));
|
const pub = map.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain));
|
||||||
if (pub) return await this.joinByInvite(pub.code);
|
if (pub) return await this.joinByInvite(pub.code);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ const hasBidder = (poll, userId) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto }) => {
|
module.exports = ({ cooler, cryptoModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
if (!ssb) ssb = await cooler.open()
|
if (!ssb) ssb = await cooler.open()
|
||||||
|
|
@ -196,9 +196,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(itemContent, (err, res) => {
|
ssbClient.publish(itemContent, (err, res) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
if (res && res.key && tribeCrypto) {
|
if (res && res.key && cryptoModel) {
|
||||||
const key = tribeCrypto.generateTribeKey()
|
const key = cryptoModel.generateKey()
|
||||||
tribeCrypto.setKey(res.key, key, 1)
|
cryptoModel.setKey(res.key, key, 1)
|
||||||
}
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
})
|
})
|
||||||
|
|
@ -353,7 +353,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
includesShipping: !!c.includesShipping,
|
includesShipping: !!c.includesShipping,
|
||||||
stock,
|
stock,
|
||||||
deadline: c.deadline || null,
|
deadline: c.deadline || null,
|
||||||
auctions_poll: (tribeCrypto && tribeCrypto.getKey(rootId)) ? poll : [],
|
auctions_poll: (cryptoModel && cryptoModel.getKey(rootId)) ? poll : [],
|
||||||
mapUrl: c.mapUrl || "",
|
mapUrl: c.mapUrl || "",
|
||||||
shopProductId: c.shopProductId || "",
|
shopProductId: c.shopProductId || "",
|
||||||
shopId: c.shopId || "",
|
shopId: c.shopId || "",
|
||||||
|
|
@ -458,7 +458,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
includesShipping: !!c.includesShipping,
|
includesShipping: !!c.includesShipping,
|
||||||
stock,
|
stock,
|
||||||
deadline: c.deadline,
|
deadline: c.deadline,
|
||||||
auctions_poll: (tribeCrypto && tribeCrypto.getKey(rootId)) ? poll : [],
|
auctions_poll: (cryptoModel && cryptoModel.getKey(rootId)) ? poll : [],
|
||||||
mapUrl: c.mapUrl || "",
|
mapUrl: c.mapUrl || "",
|
||||||
shopProductId: c.shopProductId || "",
|
shopProductId: c.shopProductId || "",
|
||||||
shopId: c.shopId || "",
|
shopId: c.shopId || "",
|
||||||
|
|
@ -505,6 +505,42 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
return changed
|
return changed
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async createOrder(itemId, details = {}) {
|
||||||
|
const ssbClient = await openSsb()
|
||||||
|
const me = ssbClient.id
|
||||||
|
const item = await this.getItemById(itemId)
|
||||||
|
if (!item) throw new Error("Item not found")
|
||||||
|
if (item.seller === me) throw new Error("You cannot buy your own item")
|
||||||
|
const content = {
|
||||||
|
type: "marketOrder",
|
||||||
|
itemId: item.rootId || item.id,
|
||||||
|
seller: item.seller,
|
||||||
|
title: String(item.title || ""),
|
||||||
|
price: String(item.price || ""),
|
||||||
|
deliveryAddress: String(details.deliveryAddress || ""),
|
||||||
|
contact: String(details.contact || ""),
|
||||||
|
notes: String(details.notes || ""),
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
return new Promise((res, rej) => ssbClient.private.publish(content, [me, item.seller], (e, m) => e ? rej(e) : res(m)))
|
||||||
|
},
|
||||||
|
|
||||||
|
async listOrders(itemId) {
|
||||||
|
const ssbClient = await openSsb()
|
||||||
|
const me = ssbClient.id
|
||||||
|
const item = await this.getItemById(itemId)
|
||||||
|
if (!item) return []
|
||||||
|
const rootId = item.rootId || item.id
|
||||||
|
const msgs = await readTyped(ssbClient, ["marketOrder"], { limit: logLimit, withPrivate: true })
|
||||||
|
return msgs
|
||||||
|
.filter(m => {
|
||||||
|
const c = m && m.value && m.value.content
|
||||||
|
return m.value.private === true && c && c.type === "marketOrder" && c.itemId === rootId && c.seller === item.seller && (item.seller === me || m.value.author === me)
|
||||||
|
})
|
||||||
|
.map(m => ({ id: m.key, buyer: m.value.author, ts: m.value.timestamp || 0, deliveryAddress: m.value.content.deliveryAddress || "", contact: m.value.content.contact || "", notes: m.value.content.notes || "" }))
|
||||||
|
.sort((a, b) => b.ts - a.ts)
|
||||||
|
},
|
||||||
|
|
||||||
async getItemByShopProductId(shopProductId) {
|
async getItemByShopProductId(shopProductId) {
|
||||||
if (!shopProductId) return null
|
if (!shopProductId) return null
|
||||||
const items = await this.listAllItems("all")
|
const items = await this.listAllItems("all")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const pull = require('../server/node_modules/pull-stream');
|
const pull = require('../server/node_modules/pull-stream');
|
||||||
const { readTyped, CONTENT_TYPES } = require('./typed_log');
|
const { readTyped, CONTENT_TYPES } = require('./typed_log');
|
||||||
|
const { isComment } = require('./comments_model');
|
||||||
|
|
||||||
const SEEN_FILE = 'oasis-mentions-seen';
|
const SEEN_FILE = 'oasis-mentions-seen';
|
||||||
|
|
||||||
|
|
@ -133,7 +134,7 @@ module.exports = ({ cooler }) => {
|
||||||
id: m.key,
|
id: m.key,
|
||||||
key: m.key,
|
key: m.key,
|
||||||
author: v.author,
|
author: v.author,
|
||||||
type: c.type,
|
type: isComment(c) ? 'comment' : c.type,
|
||||||
title: titleOf(c),
|
title: titleOf(c),
|
||||||
text: snippet(c),
|
text: snippet(c),
|
||||||
content: c,
|
content: c,
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,30 @@ module.exports = ({ cooler }) => {
|
||||||
return ssb;
|
return ssb;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasBlob = async (ssbClient, url) => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
ssbClient.blobs.has(url, (err, has) => {
|
|
||||||
resolve(!err && has);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const validTypes = [
|
const validTypes = [
|
||||||
'bookmark', 'votes', 'transfer',
|
'bookmark', 'votes', 'transfer',
|
||||||
'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file',
|
'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file',
|
||||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse',
|
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse',
|
||||||
'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'
|
'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute', 'wikiPage', 'map', 'industryBlueprint', 'job'
|
||||||
];
|
];
|
||||||
|
const OPINION_TYPES = [...new Set(Object.values(categories.messageTypes))];
|
||||||
|
|
||||||
|
const tallyOf = async (versionIds) => {
|
||||||
|
const want = new Set((versionIds || []).filter(Boolean));
|
||||||
|
const agg = { opinions: {}, opinions_inhabitants: [] };
|
||||||
|
if (!want.size) return agg;
|
||||||
|
const ssbClient = await openSsb();
|
||||||
|
const msgs = await readTyped(ssbClient, [...OPINION_TYPES, 'tombstone'], { limit: logLimit });
|
||||||
|
const gone = buildValidatedTombstoneSet(msgs);
|
||||||
|
for (const m of msgs) {
|
||||||
|
const c = m.value?.content;
|
||||||
|
if (!c || !OPINION_TYPES.includes(c.type) || !want.has(c.target) || gone.has(m.key)) continue;
|
||||||
|
if (agg.opinions_inhabitants.includes(m.value.author)) continue;
|
||||||
|
agg.opinions_inhabitants.push(m.value.author);
|
||||||
|
if (categories.includes(c.category)) agg.opinions[c.category] = (agg.opinions[c.category] || 0) + 1;
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
};
|
||||||
|
|
||||||
const getPreview = c => {
|
const getPreview = c => {
|
||||||
if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`;
|
if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`;
|
||||||
|
|
@ -55,6 +65,7 @@ module.exports = ({ cooler }) => {
|
||||||
const otype = OPINION_MSG_TYPE[type];
|
const otype = OPINION_MSG_TYPE[type];
|
||||||
if (!otype) throw new Error("Voting not allowed on this content type.");
|
if (!otype) throw new Error("Voting not allowed on this content type.");
|
||||||
if (msg.content.opinions_inhabitants?.includes(userId)) throw new Error("Already voted.");
|
if (msg.content.opinions_inhabitants?.includes(userId)) throw new Error("Already voted.");
|
||||||
|
if ((await tallyOf([contentId])).opinions_inhabitants.includes(userId)) throw new Error("Already voted.");
|
||||||
const content = { type: otype, target: contentId, category, createdAt: new Date().toISOString() };
|
const content = { type: otype, target: contentId, category, createdAt: new Date().toISOString() };
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
ssbClient.publish(content, (err, result) => err ? reject(err) : resolve(result))
|
ssbClient.publish(content, (err, result) => err ? reject(err) : resolve(result))
|
||||||
|
|
@ -77,7 +88,7 @@ module.exports = ({ cooler }) => {
|
||||||
opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category });
|
opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (c.opinions && !['task', 'event', 'report'].includes(c.type)) {
|
if ((c.opinions || categories.opinable(c)) && !['task', 'event', 'report'].includes(c.type)) {
|
||||||
byId.set(key, {
|
byId.set(key, {
|
||||||
key,
|
key,
|
||||||
value: {
|
value: {
|
||||||
|
|
@ -136,23 +147,6 @@ module.exports = ({ cooler }) => {
|
||||||
if (c.encryptedPayload) return false;
|
if (c.encryptedPayload) return false;
|
||||||
return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE';
|
return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE';
|
||||||
});
|
});
|
||||||
const blobTypes = ['document', 'image', 'audio', 'video'];
|
|
||||||
const blobCheckCache = new Map();
|
|
||||||
|
|
||||||
filtered = await Promise.all(
|
|
||||||
filtered.map(async m => {
|
|
||||||
const c = m.value.content;
|
|
||||||
if (blobTypes.includes(c.type) && c.url) {
|
|
||||||
if (!blobCheckCache.has(c.url)) {
|
|
||||||
const valid = await hasBlob(ssbClient, c.url);
|
|
||||||
blobCheckCache.set(c.url, valid);
|
|
||||||
}
|
|
||||||
if (!blobCheckCache.get(c.url)) return null;
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
filtered = filtered.filter(Boolean);
|
|
||||||
|
|
||||||
const signatureOf = (m) => {
|
const signatureOf = (m) => {
|
||||||
const c = m.value?.content || {};
|
const c = m.value?.content || {};
|
||||||
|
|
@ -267,6 +261,7 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
tallyOf,
|
||||||
createVote,
|
createVote,
|
||||||
listOpinions,
|
listOpinions,
|
||||||
getMessageById,
|
getMessageById,
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,18 @@ const INVITE_SALT = "SolarNET.HuB-pads"
|
||||||
const INVITE_BYTES = 16
|
const INVITE_BYTES = 16
|
||||||
const MEMBER_COLORS = ["#e74c3c","#3498db","#2ecc71","#f39c12","#9b59b6","#1abc9c","#e67e22","#e91e63","#00bcd4","#8bc34a"]
|
const MEMBER_COLORS = ["#e74c3c","#3498db","#2ecc71","#f39c12","#9b59b6","#1abc9c","#e67e22","#e91e63","#00bcd4","#8bc34a"]
|
||||||
|
|
||||||
module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cipherModel, cryptoModel, padCrypto, tribesModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
const ownCrypto = padCrypto || tribeCrypto
|
const ownCrypto = padCrypto || cryptoModel
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null
|
||||||
const lookupKeys = (rid) => {
|
const lookupKeys = (rid) => {
|
||||||
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
||||||
if (a.length) return a
|
if (a.length) return a
|
||||||
return (tribeCrypto && tribeCrypto.getKeys(rid)) || []
|
return (cryptoModel && cryptoModel.getKeys(rid)) || []
|
||||||
}
|
}
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0)
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0)
|
||||||
|
|
||||||
let keyringPath = null
|
let keyringPath = null
|
||||||
let migratedToTribeCrypto = false
|
let migratedToTribeCrypto = false
|
||||||
|
|
@ -60,7 +60,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
if (ownCrypto) return lookupKey(rootId)
|
if (ownCrypto) return lookupKey(rootId)
|
||||||
try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null }
|
try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null }
|
||||||
}
|
}
|
||||||
const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null)
|
const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null)
|
||||||
const inviteMatches = (inv, token) => {
|
const inviteMatches = (inv, token) => {
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
if (typeof inv === "string") return inv === token
|
if (typeof inv === "string") return inv === token
|
||||||
|
|
@ -71,7 +71,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
}
|
}
|
||||||
const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "")
|
const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "")
|
||||||
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } }
|
const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } }
|
||||||
const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(tribeCrypto && typeof tribeCrypto.getKeys === "function" ? tribeCrypto.getKeys(rootId) : [])
|
const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(cryptoModel && typeof cryptoModel.getKeys === "function" ? cryptoModel.getKeys(rootId) : [])
|
||||||
const setPadKey = (rootId, keyHex) => {
|
const setPadKey = (rootId, keyHex) => {
|
||||||
migrateLegacyKeyring()
|
migrateLegacyKeyring()
|
||||||
if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return }
|
if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return }
|
||||||
|
|
@ -113,10 +113,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTribeKeysFor = async (tribeId) => {
|
const getTribeKeysFor = async (tribeId) => {
|
||||||
if (!tribeCrypto || !tribesModel || !tribeId) return []
|
if (!cryptoModel || !tribesModel || !tribeId) return []
|
||||||
try {
|
try {
|
||||||
const rootId = await tribesModel.getRootId(tribeId)
|
const rootId = await tribesModel.getRootId(tribeId)
|
||||||
const keys = tribeCrypto.getKeys(rootId) || []
|
const keys = cryptoModel.getKeys(rootId) || []
|
||||||
return keys
|
return keys
|
||||||
} catch (_) { return [] }
|
} catch (_) { return [] }
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureMemberKeys = async (ssbClient, messages, items) => {
|
const ensureMemberKeys = async (ssbClient, messages, items) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return
|
if (!cryptoModel || !ownCrypto) return
|
||||||
const leavesByRoot = new Map()
|
const leavesByRoot = new Map()
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content
|
const c = m.value && m.value.content
|
||||||
|
|
@ -161,7 +161,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
const key = lookupKey(rootId)
|
const key = lookupKey(rootId)
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue
|
if (!Object.keys(memberKeys).length) continue
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -199,7 +199,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
const generateInviteSalt = () => crypto.randomBytes(16).toString("hex")
|
const generateInviteSalt = () => crypto.randomBytes(16).toString("hex")
|
||||||
|
|
||||||
const rotatePadKey = async (rootId, remainingMembers) => {
|
const rotatePadKey = async (rootId, remainingMembers) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rootId) return
|
if (!ownCrypto || !cryptoModel || !rootId) return
|
||||||
const existing = getPadKey(rootId)
|
const existing = getPadKey(rootId)
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
const newKey = crypto.randomBytes(32).toString("hex")
|
const newKey = crypto.randomBytes(32).toString("hex")
|
||||||
|
|
@ -208,7 +208,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -537,10 +537,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
if (!usesTribeKey) {
|
if (!usesTribeKey) {
|
||||||
setPadKey(msg.key, keyHex)
|
setPadKey(msg.key, keyHex)
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const boxedKey = tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys)
|
const boxedKey = cryptoModel.boxKeyForMember(keyHex, userId, ssbKeys)
|
||||||
ssbClient.publish({ type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve(msg))
|
ssbClient.publish({ type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve(msg))
|
||||||
return
|
return
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
@ -808,13 +808,13 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel })
|
||||||
}
|
}
|
||||||
if (padKey) setPadKey(resolvedRootId, padKey)
|
if (padKey) setPadKey(resolvedRootId, padKey)
|
||||||
await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : token)
|
await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : token)
|
||||||
if (tribeCrypto && padKey && resolvedRootId) {
|
if (cryptoModel && padKey && resolvedRootId) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(padKey, userId, ssbKeys) } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(padKey, userId, ssbKeys) } catch (_) {}
|
||||||
if (matchedPad.author && matchedPad.author !== userId) {
|
if (matchedPad.author && matchedPad.author !== userId) {
|
||||||
try { memberKeys[matchedPad.author] = tribeCrypto.boxKeyForMember(padKey, matchedPad.author, ssbKeys) } catch (_) {}
|
try { memberKeys[matchedPad.author] = cryptoModel.boxKeyForMember(padKey, matchedPad.author, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ module.exports = ({ cooler, services = {} }) => {
|
||||||
if ((n.c.replaces && nodes.has(n.c.replaces)) || !settle(n.key)) continue;
|
if ((n.c.replaces && nodes.has(n.c.replaces)) || !settle(n.key)) continue;
|
||||||
const tip = tipOf(n.key);
|
const tip = tipOf(n.key);
|
||||||
if (tomb.has(tip)) continue;
|
if (tomb.has(tip)) continue;
|
||||||
items.push({ ...nodes.get(tip).c, id: tip });
|
items.push({ ...nodes.get(tip).c, id: tip, rootId: n.key });
|
||||||
}
|
}
|
||||||
const resolve = (k) => (nodes.has(k) && settle(k) ? tipOf(rootOf(k)) : null);
|
const resolve = (k) => (nodes.has(k) && settle(k) ? tipOf(rootOf(k)) : null);
|
||||||
const authorOf = (k) => (nodes.has(k) ? nodes.get(k).author : null);
|
const authorOf = (k) => (nodes.has(k) ? nodes.get(k).author : null);
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ module.exports = ({ cooler }) => {
|
||||||
channels.set(root, {
|
channels.set(root, {
|
||||||
id: root, rootId: root, tipId: tip.key,
|
id: root, rootId: root, tipId: tip.key,
|
||||||
title: safeText(c.title), description: safeText(c.description), category: normalizeCategory(c.category),
|
title: safeText(c.title), description: safeText(c.description), category: normalizeCategory(c.category),
|
||||||
cover: parseMedia(c.cover), tags: normalizeTags(c.tags),
|
cover: parseMedia(c.cover), tags: normalizeTags(c.tags), license: typeof c.license === 'string' ? c.license : '',
|
||||||
author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(),
|
author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(),
|
||||||
updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts,
|
updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts,
|
||||||
episodes: eps, episodeCount: eps.length, playCount, opinionCount,
|
episodes: eps, episodeCount: eps.length, playCount, opinionCount,
|
||||||
|
|
@ -163,10 +163,7 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CATEGORIES,
|
async createChannel({ title, description, category, cover, tags, license }) {
|
||||||
parseMedia,
|
|
||||||
|
|
||||||
async createChannel({ title, description, category, cover, tags }) {
|
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const t = safeText(title);
|
const t = safeText(title);
|
||||||
if (!t) throw new Error('Title is required');
|
if (!t) throw new Error('Title is required');
|
||||||
|
|
@ -174,7 +171,7 @@ module.exports = ({ cooler }) => {
|
||||||
const media = parseMedia(cover);
|
const media = parseMedia(cover);
|
||||||
return publish(ssbClient, {
|
return publish(ssbClient, {
|
||||||
type: CHANNEL_TYPE, title: t, description: safeText(description), category: normalizeCategory(category),
|
type: CHANNEL_TYPE, title: t, description: safeText(description), category: normalizeCategory(category),
|
||||||
...(media ? { cover: media } : {}), tags: normalizeTags(tags), author: ssbClient.id, createdAt: now, updatedAt: now
|
...(media ? { cover: media } : {}), tags: normalizeTags(tags), ...(license ? { license } : {}), author: ssbClient.id, createdAt: now, updatedAt: now
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -183,6 +180,7 @@ module.exports = ({ cooler }) => {
|
||||||
const ch = findChannel(idx, id);
|
const ch = findChannel(idx, id);
|
||||||
if (!ch) throw new Error('Podcast not found');
|
if (!ch) throw new Error('Podcast not found');
|
||||||
if (ch.author !== ssbClient.id) throw new Error('Only the author can update this podcast');
|
if (ch.author !== ssbClient.id) throw new Error('Only the author can update this podcast');
|
||||||
|
categories.assertMediaKept(ch, data.cover !== undefined && !!data.cover);
|
||||||
const media = data.cover !== undefined && data.cover ? parseMedia(data.cover) : ch.cover;
|
const media = data.cover !== undefined && data.cover ? parseMedia(data.cover) : ch.cover;
|
||||||
const res = await publish(ssbClient, {
|
const res = await publish(ssbClient, {
|
||||||
type: CHANNEL_TYPE,
|
type: CHANNEL_TYPE,
|
||||||
|
|
@ -191,6 +189,7 @@ module.exports = ({ cooler }) => {
|
||||||
category: data.category !== undefined ? normalizeCategory(data.category) : ch.category,
|
category: data.category !== undefined ? normalizeCategory(data.category) : ch.category,
|
||||||
...(media ? { cover: media } : {}),
|
...(media ? { cover: media } : {}),
|
||||||
tags: data.tags !== undefined ? normalizeTags(data.tags) : ch.tags,
|
tags: data.tags !== undefined ? normalizeTags(data.tags) : ch.tags,
|
||||||
|
...((data.license !== undefined ? data.license : ch.license) ? { license: data.license !== undefined ? data.license : ch.license } : {}),
|
||||||
author: ssbClient.id, createdAt: ch.createdAt, updatedAt: new Date().toISOString(), replaces: ch.tipId
|
author: ssbClient.id, createdAt: ch.createdAt, updatedAt: new Date().toISOString(), replaces: ch.tipId
|
||||||
});
|
});
|
||||||
return { key: res.key, rootId: ch.id };
|
return { key: res.key, rootId: ch.id };
|
||||||
|
|
@ -228,6 +227,7 @@ module.exports = ({ cooler }) => {
|
||||||
const ep = findEpisode(idx, id);
|
const ep = findEpisode(idx, id);
|
||||||
if (!ep) throw new Error('Episode not found');
|
if (!ep) throw new Error('Episode not found');
|
||||||
if (ep.author !== ssbClient.id) throw new Error('Only the author can update this episode');
|
if (ep.author !== ssbClient.id) throw new Error('Only the author can update this episode');
|
||||||
|
categories.assertMediaKept(ep, !!data.media);
|
||||||
const m = data.media ? parseMedia(data.media) : ep.media;
|
const m = data.media ? parseMedia(data.media) : ep.media;
|
||||||
const res = await publish(ssbClient, {
|
const res = await publish(ssbClient, {
|
||||||
type: EPISODE_TYPE, channel: ep.channelId, number: data.number !== undefined && num(data.number) ? num(data.number) : ep.number,
|
type: EPISODE_TYPE, channel: ep.channelId, number: data.number !== undefined && num(data.number) ? num(data.number) : ep.number,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const { getConfig } = require('../configs/config-manager.js');
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { readTyped, memoIndex } = require('./typed_log');
|
const { readTyped, memoIndex } = require('./typed_log');
|
||||||
const { versionGroups } = require('../backend/dedupe');
|
const { versionGroups } = require('../backend/dedupe');
|
||||||
|
const opinionRules = require('../backend/opinion_categories');
|
||||||
|
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@ const normalizeTags = (raw) => {
|
||||||
return list.map(t => clean(t)).filter(Boolean).slice(0, 20);
|
return list.map(t => clean(t)).filter(Boolean).slice(0, 20);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = null }) => {
|
module.exports = ({ cooler, isPublic = false, cryptoModel = null, chatsModel = null }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
|
||||||
|
|
@ -44,10 +45,10 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n
|
||||||
const getAllMessages = async (ssbClient) => readTyped(ssbClient, POLLS_TYPES, { limit: logLimit });
|
const getAllMessages = async (ssbClient) => readTyped(ssbClient, POLLS_TYPES, { limit: logLimit });
|
||||||
|
|
||||||
const decryptField = (value, keys) => {
|
const decryptField = (value, keys) => {
|
||||||
if (!tribeCrypto || typeof value !== 'string' || !value) return null;
|
if (!cryptoModel || typeof value !== 'string' || !value) return null;
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
try {
|
try {
|
||||||
const out = tribeCrypto.decryptWithKey(value, k);
|
const out = cryptoModel.decryptWithKey(value, k);
|
||||||
if (typeof out === 'string' && out) return out;
|
if (typeof out === 'string' && out) return out;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -312,9 +313,9 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n
|
||||||
...(data.houseKey ? { houseKey: String(data.houseKey) } : {})
|
...(data.houseKey ? { houseKey: String(data.houseKey) } : {})
|
||||||
};
|
};
|
||||||
const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId });
|
const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId });
|
||||||
if (key && tribeCrypto) {
|
if (key && cryptoModel) {
|
||||||
content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key);
|
content.encryptedQuestion = cryptoModel.encryptWithKey(question, key);
|
||||||
content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key);
|
content.encryptedOptions = cryptoModel.encryptWithKey(JSON.stringify(options), key);
|
||||||
} else {
|
} else {
|
||||||
content.question = question;
|
content.question = question;
|
||||||
content.options = options;
|
content.options = options;
|
||||||
|
|
@ -338,6 +339,7 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n
|
||||||
if (!Number.isFinite(t)) throw new Error('Invalid deadline');
|
if (!Number.isFinite(t)) throw new Error('Invalid deadline');
|
||||||
}
|
}
|
||||||
const question = data.question !== undefined ? clean(data.question) : poll.question;
|
const question = data.question !== undefined ? clean(data.question) : poll.question;
|
||||||
|
opinionRules.assertMediaKept(poll, question !== poll.question || JSON.stringify(options) !== JSON.stringify(poll.options));
|
||||||
const content = {
|
const content = {
|
||||||
type: POLL_TYPE,
|
type: POLL_TYPE,
|
||||||
replaces: poll.tipId,
|
replaces: poll.tipId,
|
||||||
|
|
@ -356,9 +358,9 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n
|
||||||
...(poll.houseKey ? { houseKey: poll.houseKey } : {})
|
...(poll.houseKey ? { houseKey: poll.houseKey } : {})
|
||||||
};
|
};
|
||||||
const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId });
|
const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId });
|
||||||
if (key && tribeCrypto) {
|
if (key && cryptoModel) {
|
||||||
content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key);
|
content.encryptedQuestion = cryptoModel.encryptWithKey(question, key);
|
||||||
content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key);
|
content.encryptedOptions = cryptoModel.encryptWithKey(JSON.stringify(options), key);
|
||||||
} else {
|
} else {
|
||||||
content.question = question;
|
content.question = question;
|
||||||
content.options = options;
|
content.options = options;
|
||||||
|
|
@ -386,7 +388,7 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n
|
||||||
...(poll.chatId ? { chatId: poll.chatId } : {})
|
...(poll.chatId ? { chatId: poll.chatId } : {})
|
||||||
};
|
};
|
||||||
const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId });
|
const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId });
|
||||||
if (key && tribeCrypto) content.encryptedChoices = tribeCrypto.encryptWithKey(JSON.stringify(unique), key);
|
if (key && cryptoModel) content.encryptedChoices = cryptoModel.encryptWithKey(JSON.stringify(unique), key);
|
||||||
else content.choices = unique;
|
else content.choices = unique;
|
||||||
return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg)));
|
return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg)));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ module.exports = ({ cooler }) => {
|
||||||
const groupKeys = []
|
const groupKeys = []
|
||||||
{ let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } }
|
{ let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } }
|
||||||
|
|
||||||
const project = { ...best.c, id: tip }
|
const project = { ...best.c, id: tip, rootId: root }
|
||||||
|
|
||||||
const followers = new Set()
|
const followers = new Set()
|
||||||
const opinions = {}
|
const opinions = {}
|
||||||
|
|
|
||||||
|
|
@ -13,38 +13,11 @@ const ensureArray = (v) => Array.isArray(v) ? v.filter(Boolean) : [];
|
||||||
|
|
||||||
const trimStr = (v) => String(v || '').trim();
|
const trimStr = (v) => String(v || '').trim();
|
||||||
|
|
||||||
const normalizeTemplate = (category, tpl) => {
|
const LEGACY_TEMPLATE_KEYS = ['problemStatement', 'userStory', 'acceptanceCriteria', 'stepsToReproduce', 'expectedBehavior', 'actualBehavior', 'environment', 'whatHappened', 'reportedUser', 'contentLocation', 'whyInappropriate', 'evidenceLinks'];
|
||||||
const cat = normU(category);
|
|
||||||
|
const withLegacyText = (description, tpl) => {
|
||||||
const t = tpl && typeof tpl === 'object' ? tpl : {};
|
const t = tpl && typeof tpl === 'object' ? tpl : {};
|
||||||
|
return [trimStr(description), ...LEGACY_TEMPLATE_KEYS.map(k => trimStr(t[k]))].filter(Boolean).join('\n\n');
|
||||||
const pick = (keys) => {
|
|
||||||
const out = {};
|
|
||||||
for (const k of keys) {
|
|
||||||
const val = trimStr(t[k]);
|
|
||||||
if (val) out[k] = val;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cat === 'BUGS') {
|
|
||||||
const out = pick(['stepsToReproduce', 'expectedBehavior', 'actualBehavior', 'environment', 'reproduceRate']);
|
|
||||||
if (out.reproduceRate) out.reproduceRate = normU(out.reproduceRate);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cat === 'FEATURES') {
|
|
||||||
return pick(['problemStatement', 'userStory', 'acceptanceCriteria']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cat === 'ABUSE') {
|
|
||||||
return pick(['whatHappened', 'reportedUser', 'evidenceLinks']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cat === 'CONTENT') {
|
|
||||||
return pick(['contentLocation', 'whyInappropriate', 'evidenceLinks']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ({ cooler }) => {
|
module.exports = ({ cooler }) => {
|
||||||
|
|
@ -113,6 +86,7 @@ module.exports = ({ cooler }) => {
|
||||||
for (const cf of (confirmsByRoot.get(root) || [])) confirmSet.add(cf.author);
|
for (const cf of (confirmsByRoot.get(root) || [])) confirmSet.add(cf.author);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
root,
|
||||||
contentTip,
|
contentTip,
|
||||||
contentNode,
|
contentNode,
|
||||||
opinions,
|
opinions,
|
||||||
|
|
@ -126,7 +100,7 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildReport = (node, agg) => {
|
const buildReport = (node, agg) => {
|
||||||
const c = node.c || {};
|
const { template, ...c } = node.c || {};
|
||||||
const cat = normU(c.category);
|
const cat = normU(c.category);
|
||||||
const opinions = agg ? agg.opinions : (c.opinions || {});
|
const opinions = agg ? agg.opinions : (c.opinions || {});
|
||||||
const voters = agg ? agg.opinions_inhabitants : ensureArray(c.opinions_inhabitants);
|
const voters = agg ? agg.opinions_inhabitants : ensureArray(c.opinions_inhabitants);
|
||||||
|
|
@ -134,6 +108,8 @@ module.exports = ({ cooler }) => {
|
||||||
return {
|
return {
|
||||||
id: node.key,
|
id: node.key,
|
||||||
...c,
|
...c,
|
||||||
|
description: withLegacyText(c.description, template),
|
||||||
|
rootId: agg && agg.root ? agg.root : node.key,
|
||||||
category: cat,
|
category: cat,
|
||||||
status: normalizeStatus(c.status || 'OPEN'),
|
status: normalizeStatus(c.status || 'OPEN'),
|
||||||
images: normalizeImages(c.images && c.images.length ? c.images : c.image),
|
images: normalizeImages(c.images && c.images.length ? c.images : c.image),
|
||||||
|
|
@ -141,14 +117,13 @@ module.exports = ({ cooler }) => {
|
||||||
severity: normalizeSeverity(c.severity) || 'low',
|
severity: normalizeSeverity(c.severity) || 'low',
|
||||||
confirmations,
|
confirmations,
|
||||||
tags: ensureArray(c.tags),
|
tags: ensureArray(c.tags),
|
||||||
template: normalizeTemplate(cat, c.template || {}),
|
|
||||||
opinions,
|
opinions,
|
||||||
opinions_inhabitants: voters
|
opinions_inhabitants: voters
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}, media = {}) {
|
async createReport(title, description, category, image, tagsRaw = [], severity = 'low', media = {}) {
|
||||||
const ssb = await openSsb();
|
const ssb = await openSsb();
|
||||||
const userId = ssb.id;
|
const userId = ssb.id;
|
||||||
|
|
||||||
|
|
@ -175,7 +150,6 @@ module.exports = ({ cooler }) => {
|
||||||
confirmations: [],
|
confirmations: [],
|
||||||
severity: normalizeSeverity(severity) || 'low',
|
severity: normalizeSeverity(severity) || 'low',
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
template: normalizeTemplate(cat, template),
|
|
||||||
opinions: {},
|
opinions: {},
|
||||||
opinions_inhabitants: []
|
opinions_inhabitants: []
|
||||||
};
|
};
|
||||||
|
|
@ -193,7 +167,8 @@ module.exports = ({ cooler }) => {
|
||||||
const agg = idx.resolveGroup(root);
|
const agg = idx.resolveGroup(root);
|
||||||
if (!agg || agg.tombstoned) throw new Error('Report not found');
|
if (!agg || agg.tombstoned) throw new Error('Report not found');
|
||||||
|
|
||||||
const report = { content: agg.contentNode.c };
|
const { template: legacyTemplate, ...previous } = agg.contentNode.c || {};
|
||||||
|
const report = { content: previous };
|
||||||
const tipId = agg.contentTip;
|
const tipId = agg.contentTip;
|
||||||
|
|
||||||
if (agg.contentNode.author !== userId) throw new Error('Not the author');
|
if (agg.contentNode.author !== userId) throw new Error('Not the author');
|
||||||
|
|
@ -226,11 +201,9 @@ module.exports = ({ cooler }) => {
|
||||||
|
|
||||||
const confirmations = ensureArray(agg.confirmations);
|
const confirmations = ensureArray(agg.confirmations);
|
||||||
|
|
||||||
const baseTemplate = Object.prototype.hasOwnProperty.call(updatedContent, 'template')
|
const nextDescription = Object.prototype.hasOwnProperty.call(updatedContent, 'description')
|
||||||
? updatedContent.template
|
? trimStr(updatedContent.description)
|
||||||
: (report.content.template || {});
|
: withLegacyText(report.content.description, legacyTemplate);
|
||||||
|
|
||||||
const nextTemplate = normalizeTemplate(nextCategory, baseTemplate);
|
|
||||||
|
|
||||||
const updated = {
|
const updated = {
|
||||||
...report.content,
|
...report.content,
|
||||||
|
|
@ -247,7 +220,7 @@ module.exports = ({ cooler }) => {
|
||||||
severity: nextSeverity,
|
severity: nextSeverity,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
category: nextCategory,
|
category: nextCategory,
|
||||||
template: nextTemplate,
|
description: nextDescription,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
author: report.content.author
|
author: report.content.author
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,21 +35,21 @@ const rpcValue = (fn, ...args) => new Promise((resolve) => {
|
||||||
} catch (_) { resolve(null) }
|
} catch (_) { resolve(null) }
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, roomCrypto, tribesModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
const phone = async () => { const s = await openSsb(); return s && s.phone ? s.phone : null }
|
const phone = async () => { const s = await openSsb(); return s && s.phone ? s.phone : null }
|
||||||
|
|
||||||
const ownCrypto = roomCrypto || tribeCrypto
|
const ownCrypto = roomCrypto || cryptoModel
|
||||||
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null
|
const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null
|
||||||
const lookupKeys = (rid) => {
|
const lookupKeys = (rid) => {
|
||||||
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
|
||||||
if (a.length) return a
|
if (a.length) return a
|
||||||
return (tribeCrypto && tribeCrypto.getKeys(rid)) || []
|
return (cryptoModel && cryptoModel.getKeys(rid)) || []
|
||||||
}
|
}
|
||||||
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0)
|
const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0)
|
||||||
const setRoomKey = (rootId, keyHex) => { if (ownCrypto) ownCrypto.setKey(rootId, keyHex, 1) }
|
const setRoomKey = (rootId, keyHex) => { if (ownCrypto) ownCrypto.setKey(rootId, keyHex, 1) }
|
||||||
const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null)
|
const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null)
|
||||||
const inviteMatches = (inv, token) => {
|
const inviteMatches = (inv, token) => {
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
if (typeof inv === "string") return inv === token
|
if (typeof inv === "string") return inv === token
|
||||||
|
|
@ -87,10 +87,10 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTribeKeysFor = async (tribeId) => {
|
const getTribeKeysFor = async (tribeId) => {
|
||||||
if (!tribeCrypto || !tribesModel || !tribeId) return []
|
if (!cryptoModel || !tribesModel || !tribeId) return []
|
||||||
try {
|
try {
|
||||||
const rootId = await tribesModel.getRootId(tribeId)
|
const rootId = await tribesModel.getRootId(tribeId)
|
||||||
return tribeCrypto.getKeys(rootId) || []
|
return cryptoModel.getKeys(rootId) || []
|
||||||
} catch (_) { return [] }
|
} catch (_) { return [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
const generateInviteSalt = () => crypto.randomBytes(16).toString("hex")
|
const generateInviteSalt = () => crypto.randomBytes(16).toString("hex")
|
||||||
|
|
||||||
const rotateRoomKey = async (rootId, remainingMembers) => {
|
const rotateRoomKey = async (rootId, remainingMembers) => {
|
||||||
if (!ownCrypto || !tribeCrypto || !rootId) return
|
if (!ownCrypto || !cryptoModel || !rootId) return
|
||||||
if (!lookupKey(rootId)) return
|
if (!lookupKey(rootId)) return
|
||||||
const newKey = crypto.randomBytes(32).toString("hex")
|
const newKey = crypto.randomBytes(32).toString("hex")
|
||||||
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
const newGen = ownCrypto.addNewKey(rootId, newKey)
|
||||||
|
|
@ -151,7 +151,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) {
|
if (Object.keys(memberKeys).length) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
|
@ -163,7 +163,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
const readAll = async (ssbClient) => readTyped(ssbClient, ROOM_TYPES, { limit: logLimit })
|
const readAll = async (ssbClient) => readTyped(ssbClient, ROOM_TYPES, { limit: logLimit })
|
||||||
|
|
||||||
const ensureMemberKeys = async (ssbClient, messages, rooms) => {
|
const ensureMemberKeys = async (ssbClient, messages, rooms) => {
|
||||||
if (!tribeCrypto || !ownCrypto) return
|
if (!cryptoModel || !ownCrypto) return
|
||||||
const leavesByRoot = new Map()
|
const leavesByRoot = new Map()
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const c = m.value && m.value.content
|
const c = m.value && m.value.content
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
const key = lookupKey(rootId)
|
const key = lookupKey(rootId)
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
for (const m of plan.missing) {
|
for (const m of plan.missing) {
|
||||||
try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (!Object.keys(memberKeys).length) continue
|
if (!Object.keys(memberKeys).length) continue
|
||||||
await asP(ssbClient.publish, { type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys })
|
await asP(ssbClient.publish, { type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys })
|
||||||
|
|
@ -524,10 +524,10 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
})
|
})
|
||||||
if (!usesTribeKey) {
|
if (!usesTribeKey) {
|
||||||
setRoomKey(msg.key, keyHex)
|
setRoomKey(msg.key, keyHex)
|
||||||
if (tribeCrypto) {
|
if (cryptoModel) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys) } })
|
await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: cryptoModel.boxKeyForMember(keyHex, userId, ssbKeys) } })
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -714,13 +714,13 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
if (roomKey) setRoomKey(resolvedRootId, roomKey)
|
if (roomKey) setRoomKey(resolvedRootId, roomKey)
|
||||||
}
|
}
|
||||||
await this.addMemberToRoom(matchedRoom.rootId, userId, isPublic ? "" : token)
|
await this.addMemberToRoom(matchedRoom.rootId, userId, isPublic ? "" : token)
|
||||||
if (tribeCrypto && roomKey) {
|
if (cryptoModel && roomKey) {
|
||||||
try {
|
try {
|
||||||
const ssbKeys = require("../server/node_modules/ssb-keys")
|
const ssbKeys = require("../server/node_modules/ssb-keys")
|
||||||
const memberKeys = {}
|
const memberKeys = {}
|
||||||
try { memberKeys[userId] = tribeCrypto.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {}
|
try { memberKeys[userId] = cryptoModel.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {}
|
||||||
if (matchedRoom.author && matchedRoom.author !== userId) {
|
if (matchedRoom.author && matchedRoom.author !== userId) {
|
||||||
try { memberKeys[matchedRoom.author] = tribeCrypto.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {}
|
try { memberKeys[matchedRoom.author] = cryptoModel.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {}
|
||||||
}
|
}
|
||||||
if (Object.keys(memberKeys).length) await asP(ssbClient.publish, { type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys })
|
if (Object.keys(memberKeys).length) await asP(ssbClient.publish, { type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys })
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,7 @@ module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => {
|
||||||
if (!schoolCrypto) return null
|
if (!schoolCrypto) return null
|
||||||
let key = schoolCrypto.getKey(rootId)
|
let key = schoolCrypto.getKey(rootId)
|
||||||
if (key) return key
|
if (key) return key
|
||||||
key = schoolCrypto.generateTribeKey()
|
key = schoolCrypto.generateKey()
|
||||||
schoolCrypto.setKey(rootId, key, 1)
|
schoolCrypto.setKey(rootId, key, 1)
|
||||||
await publishKeyGrant(ssbClient, rootId, key, [ssbClient.id])
|
await publishKeyGrant(ssbClient, rootId, key, [ssbClient.id])
|
||||||
return key
|
return key
|
||||||
|
|
@ -441,7 +441,7 @@ module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => {
|
||||||
let toPublish = content
|
let toPublish = content
|
||||||
let courseKey = null
|
let courseKey = null
|
||||||
if (content.visibility === "INVITE" && schoolCrypto) {
|
if (content.visibility === "INVITE" && schoolCrypto) {
|
||||||
courseKey = schoolCrypto.generateTribeKey()
|
courseKey = schoolCrypto.generateKey()
|
||||||
toPublish = schoolCrypto.encryptContent(content, [courseKey], true)
|
toPublish = schoolCrypto.encryptContent(content, [courseKey], true)
|
||||||
}
|
}
|
||||||
const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m)))
|
const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m)))
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const longText = require('../backend/long_text');
|
||||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
||||||
module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
module.exports = ({ cooler, padsModel, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
if (!ssb) ssb = await cooler.open();
|
if (!ssb) ssb = await cooler.open();
|
||||||
|
|
@ -17,13 +17,13 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
||||||
const STANDALONE_ENCRYPTED_TYPES = new Set(['chat', 'pad', 'map', 'calendar']);
|
const STANDALONE_ENCRYPTED_TYPES = new Set(['chat', 'pad', 'map', 'calendar']);
|
||||||
|
|
||||||
const tryDecryptStandalone = (content) => {
|
const tryDecryptStandalone = (content) => {
|
||||||
if (!tribeCrypto || !content || !content.encryptedPayload) return null;
|
if (!cryptoModel || !content || !content.encryptedPayload) return null;
|
||||||
const rootCandidates = [content.calendarId, content.chatId, content.padId, content.mapId, content.roomId, content.parentId, content.dateId, content.rootId].filter(Boolean);
|
const rootCandidates = [content.calendarId, content.chatId, content.padId, content.mapId, content.roomId, content.parentId, content.dateId, content.rootId].filter(Boolean);
|
||||||
for (const cand of rootCandidates) {
|
for (const cand of rootCandidates) {
|
||||||
const keys = tribeCrypto.getKeys(cand);
|
const keys = cryptoModel.getKeys(cand);
|
||||||
if (!keys || !keys.length) continue;
|
if (!keys || !keys.length) continue;
|
||||||
try {
|
try {
|
||||||
const dec = tribeCrypto.decryptContent(content, keys.map(k => [k]));
|
const dec = cryptoModel.decryptContent(content, keys.map(k => [k]));
|
||||||
if (dec && !dec._undecryptable) return dec;
|
if (dec && !dec._undecryptable) return dec;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -31,9 +31,9 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryDecryptTribe = async (content) => {
|
const tryDecryptTribe = async (content) => {
|
||||||
if (!tribeCrypto || !tribesModel || !content || !content.encryptedPayload || !content.tribeId) return null;
|
if (!cryptoModel || !tribesModel || !content || !content.encryptedPayload || !content.tribeId) return null;
|
||||||
try {
|
try {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(content, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(content, tribesModel);
|
||||||
if (dec && !dec._undecryptable) return dec;
|
if (dec && !dec._undecryptable) return dec;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -74,7 +74,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
||||||
case 'market':
|
case 'market':
|
||||||
return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock];
|
return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock];
|
||||||
case 'bookmark':
|
case 'bookmark':
|
||||||
return [content?.author, content?.url, ...(content?.tags || []), content?.description, content?.category, content?.lastVisit];
|
return [content?.author, content?.title, content?.url, ...(content?.tags || []), content?.description, content?.category, content?.lastVisit];
|
||||||
case 'task':
|
case 'task':
|
||||||
return [content?.title, content?.description, content?.startTime, content?.endTime, content?.priority, content?.location, ...(content?.tags || []), content?.isPublic, content?.assignees?.length, content?.status, content?.author];
|
return [content?.title, content?.description, content?.startTime, content?.endTime, content?.priority, content?.location, ...(content?.tags || []), content?.isPublic, content?.assignees?.length, content?.status, content?.author];
|
||||||
case 'report':
|
case 'report':
|
||||||
|
|
@ -395,10 +395,10 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
||||||
let dec = null;
|
let dec = null;
|
||||||
if (c.tribeId) dec = await tryDecryptTribe(c);
|
if (c.tribeId) dec = await tryDecryptTribe(c);
|
||||||
if (!dec && STANDALONE_ENCRYPTED_TYPES.has(c.type)) {
|
if (!dec && STANDALONE_ENCRYPTED_TYPES.has(c.type)) {
|
||||||
const keys = tribeCrypto && tribeCrypto.getKeys ? tribeCrypto.getKeys(k) : [];
|
const keys = cryptoModel && cryptoModel.getKeys ? cryptoModel.getKeys(k) : [];
|
||||||
if (keys && keys.length) {
|
if (keys && keys.length) {
|
||||||
try {
|
try {
|
||||||
const out = tribeCrypto.decryptContent(c, keys.map(kk => [kk]));
|
const out = cryptoModel.decryptContent(c, keys.map(kk => [kk]));
|
||||||
if (out && !out._undecryptable) dec = out;
|
if (out && !out._undecryptable) dec = out;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const normalizeTags = (raw) => {
|
||||||
}
|
}
|
||||||
const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)
|
const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto }) => {
|
module.exports = ({ cooler, cryptoModel }) => {
|
||||||
let ssb
|
let ssb
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
|
||||||
|
|
||||||
|
|
@ -106,8 +106,8 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
|
|
||||||
const decryptBuyers = (val, key) => {
|
const decryptBuyers = (val, key) => {
|
||||||
if (Array.isArray(val)) return val
|
if (Array.isArray(val)) return val
|
||||||
if (typeof val === 'string' && tribeCrypto && key) {
|
if (typeof val === 'string' && cryptoModel && key) {
|
||||||
try { return JSON.parse(tribeCrypto.decryptWithKey(val, key)) } catch {}
|
try { return JSON.parse(cryptoModel.decryptWithKey(val, key)) } catch {}
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -116,25 +116,25 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const productShopId = (c) => (c && (c.shopId || c.tribeId)) || ""
|
const productShopId = (c) => (c && (c.shopId || c.tribeId)) || ""
|
||||||
|
|
||||||
const keysForRoot = (rootId) => {
|
const keysForRoot = (rootId) => {
|
||||||
if (!tribeCrypto || !rootId) return []
|
if (!cryptoModel || !rootId) return []
|
||||||
const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || []
|
const ks = (cryptoModel.getKeys && cryptoModel.getKeys(rootId)) || []
|
||||||
if (ks.length) return ks
|
if (ks.length) return ks
|
||||||
const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null
|
const k = cryptoModel.getKey ? cryptoModel.getKey(rootId) : null
|
||||||
return k ? [k] : []
|
return k ? [k] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptShopContent = (c, keyRootId) => {
|
const decryptShopContent = (c, keyRootId) => {
|
||||||
if (!isEncrypted(c)) return c
|
if (!isEncrypted(c)) return c
|
||||||
if (!tribeCrypto) return { ...c, _undecryptable: true }
|
if (!cryptoModel) return { ...c, _undecryptable: true }
|
||||||
const keys = keysForRoot(keyRootId)
|
const keys = keysForRoot(keyRootId)
|
||||||
if (!keys.length) return { ...c, _undecryptable: true }
|
if (!keys.length) return { ...c, _undecryptable: true }
|
||||||
return tribeCrypto.decryptContent(c, keys.map(k => [k]))
|
return cryptoModel.decryptContent(c, keys.map(k => [k]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptForShop = (plainContent, shopRootId) => {
|
const encryptForShop = (plainContent, shopRootId) => {
|
||||||
const keys = keysForRoot(shopRootId)
|
const keys = keysForRoot(shopRootId)
|
||||||
if (!keys.length) throw new Error("Missing shop key")
|
if (!keys.length) throw new Error("Missing shop key")
|
||||||
return tribeCrypto.encryptContent(plainContent, [keys[0]], true)
|
return cryptoModel.encryptContent(plainContent, [keys[0]], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildIndex = (messages) => {
|
const buildIndex = (messages) => {
|
||||||
|
|
@ -243,7 +243,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const undecryptable = !!c._undecryptable
|
const undecryptable = !!c._undecryptable
|
||||||
const agg = idx ? idx.aggregateFor(rootId, c) : { opinions: c.opinions || {}, opinions_inhabitants: safeArr(c.opinions_inhabitants) }
|
const agg = idx ? idx.aggregateFor(rootId, c) : { opinions: c.opinions || {}, opinions_inhabitants: safeArr(c.opinions_inhabitants) }
|
||||||
const sellerStock = Number(c.stock) || 0
|
const sellerStock = Number(c.stock) || 0
|
||||||
const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, tribeCrypto ? tribeCrypto.getKey(rootId) : null)
|
const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, cryptoModel ? cryptoModel.getKey(rootId) : null)
|
||||||
const buyers = Array.from(new Set(sellerBuyers.concat(idx ? idx.purchaseAuthorsFor(rootId) : [])))
|
const buyers = Array.from(new Set(sellerBuyers.concat(idx ? idx.purchaseAuthorsFor(rootId) : [])))
|
||||||
return {
|
return {
|
||||||
key: node.key,
|
key: node.key,
|
||||||
|
|
@ -324,14 +324,14 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
opinions_inhabitants: []
|
opinions_inhabitants: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const wantsEncryption = vis === "CLOSED" && !!tribeCrypto
|
const wantsEncryption = vis === "CLOSED" && !!cryptoModel
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (wantsEncryption) {
|
if (wantsEncryption) {
|
||||||
const shopKey = tribeCrypto.generateTribeKey()
|
const shopKey = cryptoModel.generateKey()
|
||||||
const encrypted = tribeCrypto.encryptContent(content, [shopKey], true)
|
const encrypted = cryptoModel.encryptContent(content, [shopKey], true)
|
||||||
ssbClient.publish(encrypted, (err, msg) => {
|
ssbClient.publish(encrypted, (err, msg) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
if (msg && msg.key) tribeCrypto.setKey(msg.key, shopKey, 1)
|
if (msg && msg.key) cryptoModel.setKey(msg.key, shopKey, 1)
|
||||||
resolve(msg)
|
resolve(msg)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -500,14 +500,14 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (e2e) {
|
if (e2e) {
|
||||||
let enc
|
let enc
|
||||||
try { enc = tribeCrypto.encryptContent(content, [shopKeys[0]], true) } catch (e) { return reject(e) }
|
try { enc = cryptoModel.encryptContent(content, [shopKeys[0]], true) } catch (e) { return reject(e) }
|
||||||
ssbClient.publish(enc, (err, msg) => err ? reject(err) : resolve(msg))
|
ssbClient.publish(enc, (err, msg) => err ? reject(err) : resolve(msg))
|
||||||
} else {
|
} else {
|
||||||
ssbClient.publish(content, (err, msg) => {
|
ssbClient.publish(content, (err, msg) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
if (msg && msg.key && tribeCrypto) {
|
if (msg && msg.key && cryptoModel) {
|
||||||
const key = tribeCrypto.generateTribeKey()
|
const key = cryptoModel.generateKey()
|
||||||
tribeCrypto.setKey(msg.key, key, 1)
|
cryptoModel.setKey(msg.key, key, 1)
|
||||||
}
|
}
|
||||||
resolve(msg)
|
resolve(msg)
|
||||||
})
|
})
|
||||||
|
|
@ -846,7 +846,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateInvite(shopId) {
|
async generateInvite(shopId) {
|
||||||
if (!tribeCrypto) throw new Error("Shop crypto unavailable")
|
if (!cryptoModel) throw new Error("Shop crypto unavailable")
|
||||||
const rootId = await this.resolveRootId(shopId)
|
const rootId = await this.resolveRootId(shopId)
|
||||||
const ssbClient = await openSsb()
|
const ssbClient = await openSsb()
|
||||||
const userId = ssbClient.id
|
const userId = ssbClient.id
|
||||||
|
|
@ -857,10 +857,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const keys = keysForRoot(rootId)
|
const keys = keysForRoot(rootId)
|
||||||
if (!keys.length) throw new Error("Missing shop key")
|
if (!keys.length) throw new Error("Missing shop key")
|
||||||
const code = require("crypto").randomBytes(16).toString("hex")
|
const code = require("crypto").randomBytes(16).toString("hex")
|
||||||
const salt = tribeCrypto.generateInviteSalt()
|
const salt = cryptoModel.generateInviteSalt()
|
||||||
const ek = tribeCrypto.encryptForInvite(keys[0], code, salt)
|
const ek = cryptoModel.encryptForInvite(keys[0], code, salt)
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
||||||
ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: tribeCrypto.hashInviteCode(code, salt) }, (e) => e ? reject(e) : resolve())
|
ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: cryptoModel.hashInviteCode(code, salt) }, (e) => e ? reject(e) : resolve())
|
||||||
)
|
)
|
||||||
return { code, shopId: rootId }
|
return { code, shopId: rootId }
|
||||||
},
|
},
|
||||||
|
|
@ -891,7 +891,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateOpenInvite(shopId) {
|
async generateOpenInvite(shopId) {
|
||||||
if (!tribeCrypto) throw new Error("Shop crypto unavailable")
|
if (!cryptoModel) throw new Error("Shop crypto unavailable")
|
||||||
const rootId = await this.resolveRootId(shopId)
|
const rootId = await this.resolveRootId(shopId)
|
||||||
const ssbClient = await openSsb()
|
const ssbClient = await openSsb()
|
||||||
const userId = ssbClient.id
|
const userId = ssbClient.id
|
||||||
|
|
@ -903,10 +903,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const keys = keysForRoot(rootId)
|
const keys = keysForRoot(rootId)
|
||||||
if (!keys.length) throw new Error("Missing shop key")
|
if (!keys.length) throw new Error("Missing shop key")
|
||||||
const code = require("crypto").randomBytes(16).toString("hex")
|
const code = require("crypto").randomBytes(16).toString("hex")
|
||||||
const salt = tribeCrypto.generateInviteSalt()
|
const salt = cryptoModel.generateInviteSalt()
|
||||||
const ek = tribeCrypto.encryptForInvite(keys[0], code, salt)
|
const ek = cryptoModel.encryptForInvite(keys[0], code, salt)
|
||||||
const invitePub = await new Promise((resolve, reject) =>
|
const invitePub = await new Promise((resolve, reject) =>
|
||||||
ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: tribeCrypto.hashInviteCode(code, salt), multi: 1 }, (e, r) => e ? reject(e) : resolve(r))
|
ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: cryptoModel.hashInviteCode(code, salt), multi: 1 }, (e, r) => e ? reject(e) : resolve(r))
|
||||||
)
|
)
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
||||||
ssbClient.publish({ type: "shop-open-invite", v: 1, target: rootId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (e) => e ? reject(e) : resolve())
|
ssbClient.publish({ type: "shop-open-invite", v: 1, target: rootId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (e) => e ? reject(e) : resolve())
|
||||||
|
|
@ -931,7 +931,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async joinByCode(code) {
|
async joinByCode(code) {
|
||||||
if (!tribeCrypto) throw new Error("Shop crypto unavailable")
|
if (!cryptoModel) throw new Error("Shop crypto unavailable")
|
||||||
const ssbClient = await openSsb()
|
const ssbClient = await openSsb()
|
||||||
const messages = await readAll(ssbClient)
|
const messages = await readAll(ssbClient)
|
||||||
const invTomb = new Set()
|
const invTomb = new Set()
|
||||||
|
|
@ -945,14 +945,14 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
if (!c || c.type !== "shop-invite") continue
|
if (!c || c.type !== "shop-invite") continue
|
||||||
if (invTomb.has(m.key)) continue
|
if (invTomb.has(m.key)) continue
|
||||||
try {
|
try {
|
||||||
if (tribeCrypto.hashInviteCode(code, c.salt) === c.codeHash) { matched = c; break }
|
if (cryptoModel.hashInviteCode(code, c.salt) === c.codeHash) { matched = c; break }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!matched) throw new Error("Invalid or expired invite code")
|
if (!matched) throw new Error("Invalid or expired invite code")
|
||||||
const shopKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt)
|
const shopKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt)
|
||||||
if (!shopKey) throw new Error("Could not decrypt invite")
|
if (!shopKey) throw new Error("Could not decrypt invite")
|
||||||
if (tribeCrypto.addNewKey) tribeCrypto.addNewKey(matched.target, shopKey)
|
if (cryptoModel.addNewKey) cryptoModel.addNewKey(matched.target, shopKey)
|
||||||
else tribeCrypto.setKey(matched.target, shopKey, 1)
|
else cryptoModel.setKey(matched.target, shopKey, 1)
|
||||||
return { ok: true, shopId: matched.target }
|
return { ok: true, shopId: matched.target }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -969,7 +969,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const curShop = isEncrypted(shopRaw) ? decryptShopContent(shopRaw, rootId) : shopRaw
|
const curShop = isEncrypted(shopRaw) ? decryptShopContent(shopRaw, rootId) : shopRaw
|
||||||
if (curShop._undecryptable) throw new Error("Cannot decrypt shop")
|
if (curShop._undecryptable) throw new Error("Cannot decrypt shop")
|
||||||
|
|
||||||
const wantEncrypted = tgt === "CLOSED" && !!tribeCrypto
|
const wantEncrypted = tgt === "CLOSED" && !!cryptoModel
|
||||||
|
|
||||||
const messages = await readAll(ssbClient)
|
const messages = await readAll(ssbClient)
|
||||||
const idx = buildIndex(messages)
|
const idx = buildIndex(messages)
|
||||||
|
|
@ -983,9 +983,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantEncrypted) {
|
if (wantEncrypted) {
|
||||||
const newKey = tribeCrypto.generateTribeKey()
|
const newKey = cryptoModel.generateKey()
|
||||||
if (tribeCrypto.addNewKey) tribeCrypto.addNewKey(rootId, newKey)
|
if (cryptoModel.addNewKey) cryptoModel.addNewKey(rootId, newKey)
|
||||||
else tribeCrypto.setKey(rootId, newKey, 1)
|
else cryptoModel.setKey(rootId, newKey, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const publish = (c) => new Promise((res, rej) => ssbClient.publish(c, (e, m) => e ? rej(e) : res(m)))
|
const publish = (c) => new Promise((res, rej) => ssbClient.publish(c, (e, m) => e ? rej(e) : res(m)))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const ssbDirOf = () => require('../server/ssb_config').path;
|
||||||
const { getConfig } = require('../configs/config-manager.js');
|
const { getConfig } = require('../configs/config-manager.js');
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { readTyped, CONTENT_TYPES } = require('./typed_log');
|
const { readTyped, CONTENT_TYPES } = require('./typed_log');
|
||||||
|
const { isComment } = require('./comments_model');
|
||||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
const opinionCategories = require('../backend/opinion_categories');
|
const opinionCategories = require('../backend/opinion_categories');
|
||||||
const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive);
|
const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive);
|
||||||
|
|
@ -43,7 +44,7 @@ const HIDDEN_ENVELOPE_TYPES = new Set([
|
||||||
'tribe-invite-tombstone'
|
'tribe-invite-tombstone'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
|
||||||
|
|
@ -203,6 +204,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeActionType = (a) => {
|
const normalizeActionType = (a) => {
|
||||||
|
if (isComment(a.content)) return 'comment';
|
||||||
const t = a.type || a.content?.type || inferType(a.content) || '';
|
const t = a.type || a.content?.type || inferType(a.content) || '';
|
||||||
return String(t).toLowerCase();
|
return String(t).toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
@ -387,13 +389,13 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
versionsOf[t] = [];
|
versionsOf[t] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null;
|
||||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||||
for (const m of allMsgs) {
|
for (const m of allMsgs) {
|
||||||
const k = m.key;
|
const k = m.key;
|
||||||
let c = m.value.content;
|
let c = m.value.content;
|
||||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel && cryptoModel.isSealedMsg(c)) {
|
||||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null;
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const inner = r.body;
|
const inner = r.body;
|
||||||
if (inner.k === 'tribe') {
|
if (inner.k === 'tribe') {
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,6 @@ module.exports = ({ cooler }) => {
|
||||||
},
|
},
|
||||||
async hasLocal(blobId) {
|
async hasLocal(blobId) {
|
||||||
try { return await hasLocal(await openSsb(), String(blobId || '')); } catch (_) { return false; }
|
try { return await hasLocal(await openSsb(), String(blobId || '')); } catch (_) { return false; }
|
||||||
},
|
|
||||||
async seedsFor(blobId) {
|
|
||||||
try { return (await findSeeds(await openSsb(), String(blobId || ''))).map((s) => s.id); } catch (_) { return []; }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const categories = require("../backend/opinion_categories");
|
const categories = require("../backend/opinion_categories");
|
||||||
const { readTyped, memoIndex } = require("./typed_log");
|
const { readTyped, memoIndex } = require("./typed_log");
|
||||||
|
|
@ -29,7 +30,14 @@ const parseBlobId = (blobMarkdown) => {
|
||||||
const voteSum = (opinions = {}) =>
|
const voteSum = (opinions = {}) =>
|
||||||
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
const torrentMeta = (meta) => {
|
||||||
|
const out = {};
|
||||||
|
if (meta && typeof meta.license === "string" && meta.license) out.license = meta.license;
|
||||||
|
if (meta && /^[0-9a-f]{40}$/.test(String(meta.infoHash || ""))) out.infoHash = meta.infoHash;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ({ cooler, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
|
|
||||||
const openSsb = async () => {
|
const openSsb = async () => {
|
||||||
|
|
@ -37,9 +45,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
return ssb;
|
return ssb;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null;
|
const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null;
|
||||||
const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c;
|
const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c;
|
||||||
const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c;
|
const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c;
|
||||||
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {};
|
||||||
const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {};
|
const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {};
|
||||||
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'torrent') : msgs;
|
const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'torrent') : msgs;
|
||||||
|
|
@ -134,6 +142,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
size: c.size || 0,
|
size: c.size || 0,
|
||||||
source: undec ? "" : (typeof c.source === "string" && c.source.startsWith("&") ? c.source : ""),
|
source: undec ? "" : (typeof c.source === "string" && c.source.startsWith("&") ? c.source : ""),
|
||||||
sourceName: undec ? "" : (c.sourceName || ""),
|
sourceName: undec ? "" : (c.sourceName || ""),
|
||||||
|
license: undec ? "" : (c.license || ""),
|
||||||
|
infoHash: undec ? "" : (/^[0-9a-f]{40}$/.test(String(c.infoHash || "")) ? c.infoHash : ""),
|
||||||
cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null),
|
cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null),
|
||||||
sourceMime: undec ? "" : (c.sourceMime || ""),
|
sourceMime: undec ? "" : (c.sourceMime || ""),
|
||||||
opinions,
|
opinions,
|
||||||
|
|
@ -169,7 +179,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
return root;
|
return root;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null) {
|
async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null, meta = {}) {
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
const blobId = parseBlobId(blobMarkdown);
|
const blobId = parseBlobId(blobMarkdown);
|
||||||
const tags = normalizeTags(tagsRaw) || [];
|
const tags = normalizeTags(tagsRaw) || [];
|
||||||
|
|
@ -191,10 +201,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
...(source && typeof source.source === "string" && source.source.startsWith("&")
|
...(source && typeof source.source === "string" && source.source.startsWith("&")
|
||||||
? { source: source.source, sourceName: String(source.sourceName || ""), sourceMime: String(source.sourceMime || "") }
|
? { source: source.source, sourceName: String(source.sourceName || ""), sourceMime: String(source.sourceMime || "") }
|
||||||
: {}),
|
: {}),
|
||||||
...cipherField(source && source.cipher)
|
...cipherField(source && source.cipher),
|
||||||
|
...torrentMeta(meta)
|
||||||
};
|
};
|
||||||
|
|
||||||
content = await encryptIfTribe(content);
|
content = await encryptIfGroup(content);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res)));
|
ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res)));
|
||||||
|
|
@ -208,12 +219,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const oldMsg = await getMsg(ssbClient, tipId);
|
const oldMsg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!oldMsg) throw new Error("Torrent not found");
|
if (!oldMsg) throw new Error("Torrent not found");
|
||||||
const oldDec = await decryptIfTribe(oldMsg.content);
|
const oldDec = await decryptIfGroup(oldMsg.content);
|
||||||
if (!oldDec || oldDec.type !== "torrent") throw new Error("Torrent not found");
|
if (!oldDec || oldDec.type !== "torrent") throw new Error("Torrent not found");
|
||||||
assertReadable(oldDec, "Torrent");
|
assertReadable(oldDec, "Torrent");
|
||||||
if (oldMsg.author !== userId) throw new Error("Not the author");
|
if (oldMsg.author !== userId) throw new Error("Not the author");
|
||||||
const aggTorrent = await this.getTorrentById(id, userId);
|
const aggTorrent = await this.getTorrentById(id, userId);
|
||||||
if (aggTorrent && Object.keys(aggTorrent.opinions || {}).some(k => (aggTorrent.opinions[k] || 0) > 0)) throw new Error("Cannot edit torrent after it has received opinions.");
|
opinionRules.assertMediaKept(aggTorrent, !!blobMarkdown);
|
||||||
|
|
||||||
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags);
|
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags);
|
||||||
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
||||||
|
|
@ -227,7 +238,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
tags,
|
tags,
|
||||||
title: title !== undefined ? title || "" : oldDec.title || "",
|
title: title !== undefined ? title || "" : oldDec.title || "",
|
||||||
description: description !== undefined ? description || "" : oldDec.description || "",
|
description: description !== undefined ? description || "" : oldDec.description || "",
|
||||||
size: oldDec.size || 0,
|
size: blobId && extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0),
|
||||||
|
...torrentMeta({ license: oldDec.license, infoHash: blobId ? "" : oldDec.infoHash, ...(extra || {}) }),
|
||||||
opinions: oldDec.opinions || {},
|
opinions: oldDec.opinions || {},
|
||||||
opinions_inhabitants: oldDec.opinions_inhabitants || [],
|
opinions_inhabitants: oldDec.opinions_inhabitants || [],
|
||||||
author: oldDec.author || userId,
|
author: oldDec.author || userId,
|
||||||
|
|
@ -236,7 +248,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
};
|
};
|
||||||
|
|
||||||
updated = await encryptIfTribe(updated);
|
updated = await encryptIfGroup(updated);
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res)));
|
ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res)));
|
||||||
|
|
@ -253,7 +265,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tipId);
|
const msg = await getMsg(ssbClient, tipId);
|
||||||
|
|
||||||
if (!msg) throw new Error("Torrent not found");
|
if (!msg) throw new Error("Torrent not found");
|
||||||
const dec = await decryptIfTribe(msg.content);
|
const dec = await decryptIfGroup(msg.content);
|
||||||
if (!dec || dec.type !== "torrent") throw new Error("Torrent not found");
|
if (!dec || dec.type !== "torrent") throw new Error("Torrent not found");
|
||||||
if (msg.author !== userId) throw new Error("Not the author");
|
if (msg.author !== userId) throw new Error("Not the author");
|
||||||
|
|
||||||
|
|
@ -332,8 +344,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
const msg = await getMsg(ssbClient, tip);
|
const msg = await getMsg(ssbClient, tip);
|
||||||
if (!msg) throw new Error("Torrent not found");
|
if (!msg) throw new Error("Torrent not found");
|
||||||
let c = msg.content;
|
let c = msg.content;
|
||||||
if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) {
|
if (cryptoModel && (c?.encryptedPayload || cryptoModel.isSealedMsg(c)) && tribesModel) {
|
||||||
const dec = await tribeCrypto.decryptFromTribe(c, tribesModel);
|
const dec = await cryptoModel.decryptFromGroup(c, tribesModel);
|
||||||
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false };
|
||||||
}
|
}
|
||||||
if (!c || c.type !== "torrent") throw new Error("Torrent not found");
|
if (!c || c.type !== "torrent") throw new Error("Torrent not found");
|
||||||
|
|
@ -357,3 +369,99 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BENCODE_MAX_DEPTH = 64;
|
||||||
|
const TORRENT_FILES_SHOWN = 50;
|
||||||
|
const MAGNET_TRACKERS_MAX = 8;
|
||||||
|
const MAGNET_SEEDS_MAX = 4;
|
||||||
|
const TORRENT_LINK_RE = /^(https?|udp|wss?):\/\//i;
|
||||||
|
|
||||||
|
const decodeBencode = (buf) => {
|
||||||
|
let pos = 0;
|
||||||
|
let infoRange = null;
|
||||||
|
const fail = () => { throw new Error("Invalid torrent"); };
|
||||||
|
const parse = (depth) => {
|
||||||
|
if (depth > BENCODE_MAX_DEPTH || pos >= buf.length) fail();
|
||||||
|
const ch = buf[pos];
|
||||||
|
if (ch === 0x69) {
|
||||||
|
const end = buf.indexOf(0x65, pos);
|
||||||
|
if (end < 0) fail();
|
||||||
|
const n = Number(buf.toString("latin1", pos + 1, end));
|
||||||
|
pos = end + 1;
|
||||||
|
return Number.isFinite(n) ? n : fail();
|
||||||
|
}
|
||||||
|
if (ch === 0x6c) {
|
||||||
|
pos++;
|
||||||
|
const out = [];
|
||||||
|
while (buf[pos] !== 0x65) out.push(parse(depth + 1));
|
||||||
|
pos++;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (ch === 0x64) {
|
||||||
|
pos++;
|
||||||
|
const out = {};
|
||||||
|
while (buf[pos] !== 0x65) {
|
||||||
|
const key = parse(depth + 1);
|
||||||
|
if (!Buffer.isBuffer(key)) fail();
|
||||||
|
const start = pos;
|
||||||
|
const name = key.toString("latin1");
|
||||||
|
out[name] = parse(depth + 1);
|
||||||
|
if (depth === 0 && name === "info") infoRange = [start, pos];
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (ch >= 0x30 && ch <= 0x39) {
|
||||||
|
const colon = buf.indexOf(0x3a, pos);
|
||||||
|
if (colon < 0) fail();
|
||||||
|
const len = Number(buf.toString("latin1", pos, colon));
|
||||||
|
const start = colon + 1;
|
||||||
|
if (!Number.isInteger(len) || len < 0 || start + len > buf.length) fail();
|
||||||
|
pos = start + len;
|
||||||
|
return buf.subarray(start, pos);
|
||||||
|
}
|
||||||
|
return fail();
|
||||||
|
};
|
||||||
|
return { value: parse(0), infoRange };
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = (v) => (Buffer.isBuffer(v) ? v.toString("utf8") : "");
|
||||||
|
|
||||||
|
const parseTorrent = (buf) => {
|
||||||
|
const { value, infoRange } = decodeBencode(Buffer.from(buf));
|
||||||
|
const info = value && value.info;
|
||||||
|
if (!info || typeof info !== "object" || !infoRange) throw new Error("Invalid torrent");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const raw = Buffer.from(buf).subarray(infoRange[0], infoRange[1]);
|
||||||
|
const v2Only = !info.pieces && info["file tree"];
|
||||||
|
const infoHash = crypto.createHash(v2Only ? "sha256" : "sha1").update(raw).digest("hex");
|
||||||
|
const name = text(info["name.utf-8"] || info.name);
|
||||||
|
const files = Array.isArray(info.files)
|
||||||
|
? info.files.map(f => ({ path: (Array.isArray(f["path.utf-8"] || f.path) ? (f["path.utf-8"] || f.path) : []).map(text).join("/"), length: Number(f.length) || 0 }))
|
||||||
|
: [{ path: name, length: Number(info.length) || 0 }];
|
||||||
|
const announce = [text(value.announce), ...(Array.isArray(value["announce-list"]) ? value["announce-list"].flat().map(text) : [])];
|
||||||
|
const seeds = Array.isArray(value["url-list"]) ? value["url-list"].map(text) : [text(value["url-list"])];
|
||||||
|
return {
|
||||||
|
infoHash,
|
||||||
|
v2: !!v2Only,
|
||||||
|
name,
|
||||||
|
length: files.reduce((sum, f) => sum + f.length, 0),
|
||||||
|
fileCount: files.length,
|
||||||
|
files: files.slice(0, TORRENT_FILES_SHOWN),
|
||||||
|
trackers: [...new Set(announce.filter(t => TORRENT_LINK_RE.test(t)))].slice(0, MAGNET_TRACKERS_MAX),
|
||||||
|
webSeeds: [...new Set(seeds.filter(u => /^https?:\/\//i.test(u)))].slice(0, MAGNET_SEEDS_MAX)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const magnetFor = (t) => {
|
||||||
|
if (!t || !t.infoHash) return "";
|
||||||
|
const params = [t.v2 ? `xt=urn:btmh:1220${t.infoHash}` : `xt=urn:btih:${t.infoHash}`];
|
||||||
|
if (t.name) params.push(`dn=${encodeURIComponent(t.name)}`);
|
||||||
|
if (t.length) params.push(`xl=${t.length}`);
|
||||||
|
for (const tr of t.trackers || []) params.push(`tr=${encodeURIComponent(tr)}`);
|
||||||
|
for (const ws of t.webSeeds || []) params.push(`ws=${encodeURIComponent(ws)}`);
|
||||||
|
return `magnet:?${params.join("&")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.parseTorrent = parseTorrent;
|
||||||
|
module.exports.magnetFor = magnetFor;
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ module.exports = ({ cooler }) => {
|
||||||
for (const cf of rootConfirms) { if (cf.author === best.c.to) confirmedBy.add(cf.author) }
|
for (const cf of rootConfirms) { if (cf.author === best.c.to) confirmedBy.add(cf.author) }
|
||||||
for (const op of (opinionsByRoot.get(root) || [])) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } }
|
for (const op of (opinionsByRoot.get(root) || [])) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } }
|
||||||
const ubiPayerTrusted = !!best.author && (best.author === defaultPub || !!(claimedPubs.get(best.c.to) && claimedPubs.get(best.c.to).has(best.author)))
|
const ubiPayerTrusted = !!best.author && (best.author === defaultPub || !!(claimedPubs.get(best.c.to) && claimedPubs.get(best.c.to).has(best.author)))
|
||||||
return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted }
|
return { root, contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }
|
return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }
|
||||||
|
|
@ -204,6 +204,7 @@ module.exports = ({ cooler }) => {
|
||||||
const opinions_inhabitants = agg ? agg.opinions_inhabitants : (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])
|
const opinions_inhabitants = agg ? agg.opinions_inhabitants : (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])
|
||||||
return {
|
return {
|
||||||
id: node.key,
|
id: node.key,
|
||||||
|
rootId: agg && agg.root ? agg.root : node.key,
|
||||||
author: node.author,
|
author: node.author,
|
||||||
from: c.from,
|
from: c.from,
|
||||||
to: c.to,
|
to: c.to,
|
||||||
|
|
@ -289,7 +290,6 @@ module.exports = ({ cooler }) => {
|
||||||
const aggU = gU ? buildTransfer(gU.best, gU) : buildTransfer({ key: tipId, c: current })
|
const aggU = gU ? buildTransfer(gU.best, gU) : buildTransfer({ key: tipId, c: current })
|
||||||
const currentStatus = aggU.status
|
const currentStatus = aggU.status
|
||||||
|
|
||||||
if (Object.keys(aggU.opinions || {}).some(k => (aggU.opinions[k] || 0) > 0)) throw new Error("Cannot edit transfer after it has received opinions.")
|
|
||||||
if (current.from !== userId) throw new Error("Not the author")
|
if (current.from !== userId) throw new Error("Not the author")
|
||||||
if (currentStatus !== "UNCONFIRMED") throw new Error("Can only edit unconfirmed")
|
if (currentStatus !== "UNCONFIRMED") throw new Error("Can only edit unconfirmed")
|
||||||
|
|
||||||
|
|
@ -300,6 +300,7 @@ module.exports = ({ cooler }) => {
|
||||||
|
|
||||||
const num = parseNum(amount)
|
const num = parseNum(amount)
|
||||||
if (!Number.isFinite(num) || num <= 0) throw new Error("Amount must be positive")
|
if (!Number.isFinite(num) || num <= 0) throw new Error("Amount must be positive")
|
||||||
|
categories.assertMediaKept(aggU, to !== current.to || num.toFixed(6) !== Number(current.amount || 0).toFixed(6))
|
||||||
|
|
||||||
const dl = moment(deadline, moment.ISO_8601, true)
|
const dl = moment(deadline, moment.ISO_8601, true)
|
||||||
if (!dl.isValid() || dl.isBefore(moment())) throw new Error("Deadline must be in the future")
|
if (!dl.isValid() || dl.isBefore(moment())) throw new Error("Deadline must be in the future")
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,11 @@ module.exports = ({ cooler }) => {
|
||||||
return ssb;
|
return ssb;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasBlob = async (ssbClient, url) => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
ssbClient.blobs.has(url, (err, has) => resolve(!err && has));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const types = [
|
const types = [
|
||||||
'bookmark', 'votes', 'poll', 'feed', 'post',
|
'bookmark', 'votes', 'poll', 'feed', 'post',
|
||||||
'image', 'audio', 'video', 'document', 'torrent', 'file', 'transfer',
|
'image', 'audio', 'video', 'document', 'torrent', 'file', 'transfer',
|
||||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse',
|
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse',
|
||||||
'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'
|
'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute', 'wikiPage', 'map', 'industryBlueprint', 'job'
|
||||||
];
|
];
|
||||||
|
|
||||||
const categories = opinionCategories;
|
const categories = opinionCategories;
|
||||||
|
|
@ -55,7 +49,7 @@ module.exports = ({ cooler }) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.opinions && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m);
|
if ((c.opinions || categories.opinable(c)) && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m);
|
||||||
|
|
||||||
if (c.type === 'feed' && !itemsById.has(k)) itemsById.set(k, m);
|
if (c.type === 'feed' && !itemsById.has(k)) itemsById.set(k, m);
|
||||||
if (isRootBlogPost(c) && !itemsById.has(k)) itemsById.set(k, m);
|
if (isRootBlogPost(c) && !itemsById.has(k)) itemsById.set(k, m);
|
||||||
|
|
@ -101,19 +95,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (c.encryptedPayload) return false;
|
if (c.encryptedPayload) return false;
|
||||||
return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE';
|
return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE';
|
||||||
});
|
});
|
||||||
const blobTypes = ['document', 'image', 'audio', 'video'];
|
let items = rawItems.slice();
|
||||||
|
|
||||||
let items = await Promise.all(
|
|
||||||
rawItems.map(async m => {
|
|
||||||
const c = m.value?.content;
|
|
||||||
if (blobTypes.includes(c.type) && c.url) {
|
|
||||||
const valid = await hasBlob(ssbClient, c.url);
|
|
||||||
if (!valid) return null;
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
items = items.filter(Boolean);
|
|
||||||
|
|
||||||
const signatureOf = (m) => {
|
const signatureOf = (m) => {
|
||||||
const c = m.value?.content || {};
|
const c = m.value?.content || {};
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const snapshotOf = (item, replies = []) => {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
module.exports = ({ cooler, cryptoModel, tribesModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
|
||||||
|
|
@ -38,15 +38,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
|
|
||||||
const fingerprintsForRoot = (rootId) => {
|
const fingerprintsForRoot = (rootId) => {
|
||||||
const fps = new Set();
|
const fps = new Set();
|
||||||
for (const k of tribeCrypto.getKeys(rootId)) fps.add(tribeCrypto.fingerprint(k));
|
for (const k of cryptoModel.getKeys(rootId)) fps.add(cryptoModel.fingerprint(k));
|
||||||
return fps;
|
return fps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapAndPublishContent = async (rootId, body) => {
|
const wrapAndPublishContent = async (rootId, body) => {
|
||||||
const client = await openSsb();
|
const client = await openSsb();
|
||||||
const key = tribeCrypto.getKey(rootId);
|
const key = cryptoModel.getKey(rootId);
|
||||||
if (!key) throw new Error('Missing tribe key for ' + rootId);
|
if (!key) throw new Error('Missing tribe key for ' + rootId);
|
||||||
const envelope = tribeCrypto.wrapMsg(body, key);
|
const envelope = cryptoModel.wrapMsg(body, key);
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
||||||
);
|
);
|
||||||
|
|
@ -59,7 +59,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const decodeContentMsgs = async (msgs, opts) => {
|
const decodeContentMsgs = async (msgs, opts) => {
|
||||||
const fpIdx = tribeCrypto.buildFingerprintIndex();
|
const fpIdx = cryptoModel.buildFingerprintIndex();
|
||||||
const targetRootId = opts && opts.rootId ? opts.rootId : null;
|
const targetRootId = opts && opts.rootId ? opts.rootId : null;
|
||||||
const wantType = opts && opts.contentType ? opts.contentType : null;
|
const wantType = opts && opts.contentType ? opts.contentType : null;
|
||||||
const allowedFps = targetRootId ? fingerprintsForRoot(targetRootId) : null;
|
const allowedFps = targetRootId ? fingerprintsForRoot(targetRootId) : null;
|
||||||
|
|
@ -72,10 +72,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
for (const m of msgs) {
|
for (const m of msgs) {
|
||||||
const c = m.value && m.value.content;
|
const c = m.value && m.value.content;
|
||||||
if (!c) continue;
|
if (!c) continue;
|
||||||
if (!tribeCrypto.isTribeMsg(c)) continue;
|
if (!cryptoModel.isSealedMsg(c)) continue;
|
||||||
if (allowedFps && !allowedFps.has(c.fp)) continue;
|
if (allowedFps && !allowedFps.has(c.fp)) continue;
|
||||||
|
|
||||||
const r = tribeCrypto.unwrapMsg(c, fpIdx);
|
const r = cryptoModel.unwrapMsg(c, fpIdx);
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
const b = r.body;
|
const b = r.body;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const VALID_INVITE_MODES = ['strict', 'open'];
|
||||||
|
|
||||||
const STRUCTURAL_FIELDS = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous', 'inviteMode', 'status', 'parentTribeId', 'mapUrl'];
|
const STRUCTURAL_FIELDS = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous', 'inviteMode', 'status', 'parentTribeId', 'mapUrl'];
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto }) => {
|
module.exports = ({ cooler, cryptoModel }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const v = m && m.value;
|
const v = m && m.value;
|
||||||
if (!v) continue;
|
if (!v) continue;
|
||||||
const c = v.content;
|
const c = v.content;
|
||||||
const dec = (c && typeof c === 'object') ? (v.private === true && tribeCrypto.isKeyDistribContent(c) ? c : null) : tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys);
|
const dec = (c && typeof c === 'object') ? (v.private === true && cryptoModel.isKeyDistribContent(c) ? c : null) : cryptoModel.tryUnboxKeyDistrib(c, config.keys, ssbKeys);
|
||||||
if (dec) out.push({ m, dec });
|
if (dec) out.push({ m, dec });
|
||||||
}
|
}
|
||||||
return { msgs, distribs: out };
|
return { msgs, distribs: out };
|
||||||
|
|
@ -79,7 +79,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const buildTribeIndex = async () => {
|
const buildTribeIndex = async () => {
|
||||||
subscribeInvalidation().catch(() => {});
|
subscribeInvalidation().catch(() => {});
|
||||||
if (tribeIndex && Date.now() - tribeIndexTs < 5000) return tribeIndex;
|
if (tribeIndex && Date.now() - tribeIndexTs < 5000) return tribeIndex;
|
||||||
const fpIdx = tribeCrypto.buildFingerprintIndex();
|
const fpIdx = cryptoModel.buildFingerprintIndex();
|
||||||
const msgs = await streamLog();
|
const msgs = await streamLog();
|
||||||
|
|
||||||
const tribeMsgs = new Map();
|
const tribeMsgs = new Map();
|
||||||
|
|
@ -102,8 +102,8 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = null;
|
let body = null;
|
||||||
if (tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel.isSealedMsg(c)) {
|
||||||
const r = tribeCrypto.unwrapMsg(c, fpIdx);
|
const r = cryptoModel.unwrapMsg(c, fpIdx);
|
||||||
if (!r || !r.body || !r.body.k) continue;
|
if (!r || !r.body || !r.body.k) continue;
|
||||||
if (r.body.k === 'open-invite' || r.body.k === 'open-invite-tombstone') {
|
if (r.body.k === 'open-invite' || r.body.k === 'open-invite-tombstone') {
|
||||||
sealedInvites.push({ key: m.key, author, body: r.body, keyRoot: r.rootId, ts });
|
sealedInvites.push({ key: m.key, author, body: r.body, keyRoot: r.rootId, ts });
|
||||||
|
|
@ -280,7 +280,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const c = m && m.value && m.value.content;
|
const c = m && m.value && m.value.content;
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
if (typeof c === 'string' && c.endsWith('.box')) { tribeIndex = null; return; }
|
if (typeof c === 'string' && c.endsWith('.box')) { tribeIndex = null; return; }
|
||||||
if (tribeCrypto.isTribeMsg(c)) { tribeIndex = null; return; }
|
if (cryptoModel.isSealedMsg(c)) { tribeIndex = null; return; }
|
||||||
if (c.type === 'tribe-invite-msg' || c.type === 'tribe-invite-tombstone') tribeIndex = null;
|
if (c.type === 'tribe-invite-msg' || c.type === 'tribe-invite-tombstone') tribeIndex = null;
|
||||||
}, () => { started = false; })
|
}, () => { started = false; })
|
||||||
);
|
);
|
||||||
|
|
@ -314,9 +314,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
|
|
||||||
const wrapAndPublish = async (rootId, body) => {
|
const wrapAndPublish = async (rootId, body) => {
|
||||||
const client = await openSsb();
|
const client = await openSsb();
|
||||||
const key = tribeCrypto.getKey(rootId);
|
const key = cryptoModel.getKey(rootId);
|
||||||
if (!key) throw new Error('Missing tribe key for ' + rootId);
|
if (!key) throw new Error('Missing tribe key for ' + rootId);
|
||||||
const envelope = tribeCrypto.wrapMsg(body, key);
|
const envelope = cryptoModel.wrapMsg(body, key);
|
||||||
const result = await new Promise((resolve, reject) =>
|
const result = await new Promise((resolve, reject) =>
|
||||||
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
||||||
);
|
);
|
||||||
|
|
@ -389,18 +389,18 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue;
|
if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue;
|
||||||
const multi = c.multi === 1 || c.multi === true;
|
const multi = c.multi === 1 || c.multi === true;
|
||||||
if (!multi && inviteTombstonedBy.has(m.key)) continue;
|
if (!multi && inviteTombstonedBy.has(m.key)) continue;
|
||||||
if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue;
|
if (cryptoModel.hashInviteCode(code, c.s) !== c.ch) continue;
|
||||||
const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s, 3);
|
const chain = cryptoModel.decryptChainFromInvite(c.ek, code, c.s, 3);
|
||||||
if (Array.isArray(chain) && chain.length) {
|
if (Array.isArray(chain) && chain.length) {
|
||||||
matched = { msgKey: m.key, codeHash: c.ch, chain, multi, inviteAuthor: v.author };
|
matched = { msgKey: m.key, codeHash: c.ch, chain, multi, inviteAuthor: v.author };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!matched) return null;
|
if (!matched) return null;
|
||||||
const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !tribeCrypto.getKeys(entry.rootId).length);
|
const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !cryptoModel.getKeys(entry.rootId).length);
|
||||||
for (const entry of fresh) tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
for (const entry of fresh) cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
||||||
tribeIndex = null;
|
tribeIndex = null;
|
||||||
const undo = () => { for (const entry of fresh) tribeCrypto.dropKey(entry.rootId); tribeIndex = null; };
|
const undo = () => { for (const entry of fresh) cryptoModel.dropKey(entry.rootId); tribeIndex = null; };
|
||||||
const rootId = matched.chain[0].rootId;
|
const rootId = matched.chain[0].rootId;
|
||||||
const idx = await buildTribeIndex().catch(() => null);
|
const idx = await buildTribeIndex().catch(() => null);
|
||||||
const entry = idx && idx.tribes.get(idx.tipByRoot.get(rootId) || rootId);
|
const entry = idx && idx.tribes.get(idx.tipByRoot.get(rootId) || rootId);
|
||||||
|
|
@ -428,7 +428,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
: String(tagsRaw || '').split(',').map(t => t.trim()).filter(Boolean);
|
: String(tagsRaw || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
|
||||||
const isPrivate = Boolean(isAnonymous);
|
const isPrivate = Boolean(isAnonymous);
|
||||||
const newKey = tribeCrypto.generateTribeKey();
|
const newKey = cryptoModel.generateKey();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const baseFields = {
|
const baseFields = {
|
||||||
title,
|
title,
|
||||||
|
|
@ -449,14 +449,14 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
};
|
};
|
||||||
let envelope;
|
let envelope;
|
||||||
if (isPrivate) {
|
if (isPrivate) {
|
||||||
envelope = tribeCrypto.wrapMsg({ k: 'tribe', op: 'create', rootId: null, replaces: null, ...baseFields }, newKey);
|
envelope = cryptoModel.wrapMsg({ k: 'tribe', op: 'create', rootId: null, replaces: null, ...baseFields }, newKey);
|
||||||
} else {
|
} else {
|
||||||
envelope = { type: 'tribe', op: 'create', ...baseFields };
|
envelope = { type: 'tribe', op: 'create', ...baseFields };
|
||||||
}
|
}
|
||||||
const result = await new Promise((resolve, reject) =>
|
const result = await new Promise((resolve, reject) =>
|
||||||
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
client.publish(envelope, (err, r) => err ? reject(err) : resolve(r))
|
||||||
);
|
);
|
||||||
tribeCrypto.setKey(result.key, newKey, 1);
|
cryptoModel.setKey(result.key, newKey, 1);
|
||||||
tribeIndex = null;
|
tribeIndex = null;
|
||||||
subscribeInvalidation().catch(() => {});
|
subscribeInvalidation().catch(() => {});
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -710,10 +710,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex');
|
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex');
|
||||||
const targetRoot = await this.getRootId(tribeId);
|
const targetRoot = await this.getRootId(tribeId);
|
||||||
if (!targetRoot) throw new Error('Cannot resolve tribe root');
|
if (!targetRoot) throw new Error('Cannot resolve tribe root');
|
||||||
const salt = tribeCrypto.generateInviteSalt();
|
const salt = cryptoModel.generateInviteSalt();
|
||||||
const ekChain = tribeCrypto.encryptChainForInvite([targetRoot], code, salt);
|
const ekChain = cryptoModel.encryptChainForInvite([targetRoot], code, salt);
|
||||||
if (!ekChain) throw new Error('Cannot encrypt invite chain — missing keys');
|
if (!ekChain) throw new Error('Cannot encrypt invite chain — missing keys');
|
||||||
const codeHash = tribeCrypto.hashInviteCode(code, salt);
|
const codeHash = cryptoModel.hashInviteCode(code, salt);
|
||||||
const inviteMsg = {
|
const inviteMsg = {
|
||||||
type: 'tribe-invite-msg',
|
type: 'tribe-invite-msg',
|
||||||
v: 1,
|
v: 1,
|
||||||
|
|
@ -727,7 +727,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const inviteRef = {
|
const inviteRef = {
|
||||||
codeHash,
|
codeHash,
|
||||||
salt,
|
salt,
|
||||||
gen: tribeCrypto.getGen(targetRoot),
|
gen: cryptoModel.getGen(targetRoot),
|
||||||
msgKey: invitePub.key
|
msgKey: invitePub.key
|
||||||
};
|
};
|
||||||
const invites = Array.isArray(tribe.invites) ? [...tribe.invites, inviteRef] : [inviteRef];
|
const invites = Array.isArray(tribe.invites) ? [...tribe.invites, inviteRef] : [inviteRef];
|
||||||
|
|
@ -770,10 +770,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex');
|
const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex');
|
||||||
const targetRoot = await this.getRootId(tribeId);
|
const targetRoot = await this.getRootId(tribeId);
|
||||||
if (!targetRoot) throw new Error('Cannot resolve tribe root');
|
if (!targetRoot) throw new Error('Cannot resolve tribe root');
|
||||||
const salt = tribeCrypto.generateInviteSalt();
|
const salt = cryptoModel.generateInviteSalt();
|
||||||
const ekChain = tribeCrypto.encryptChainForInvite([targetRoot], code, salt);
|
const ekChain = cryptoModel.encryptChainForInvite([targetRoot], code, salt);
|
||||||
if (!ekChain) throw new Error('Cannot encrypt invite chain — missing keys');
|
if (!ekChain) throw new Error('Cannot encrypt invite chain — missing keys');
|
||||||
const codeHash = tribeCrypto.hashInviteCode(code, salt);
|
const codeHash = cryptoModel.hashInviteCode(code, salt);
|
||||||
const invitePub = await new Promise((resolve, reject) =>
|
const invitePub = await new Promise((resolve, reject) =>
|
||||||
client.publish({ type: 'tribe-invite-msg', v: 1, multi: 1, ch: codeHash, s: salt, ek: ekChain }, (err, r) => err ? reject(err) : resolve(r))
|
client.publish({ type: 'tribe-invite-msg', v: 1, multi: 1, ch: codeHash, s: salt, ek: ekChain }, (err, r) => err ? reject(err) : resolve(r))
|
||||||
);
|
);
|
||||||
|
|
@ -843,8 +843,8 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
}
|
}
|
||||||
for (const entry of matched.chain) {
|
for (const entry of matched.chain) {
|
||||||
if (!entry || !Array.isArray(entry.keys) || !entry.keys.length) continue;
|
if (!entry || !Array.isArray(entry.keys) || !entry.keys.length) continue;
|
||||||
if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) tribeCrypto.dropKey(entry.rootId); continue; }
|
if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) cryptoModel.dropKey(entry.rootId); continue; }
|
||||||
if (lineage.has(entry.rootId)) tribeCrypto.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
if (lineage.has(entry.rootId)) cryptoModel.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length);
|
||||||
}
|
}
|
||||||
tribeIndex = null;
|
tribeIndex = null;
|
||||||
const members = [...tribe.members, userId];
|
const members = [...tribe.members, userId];
|
||||||
|
|
@ -894,12 +894,12 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
if (!Array.isArray(toMembers) || !toMembers.length) return;
|
if (!Array.isArray(toMembers) || !toMembers.length) return;
|
||||||
const client = await openSsb();
|
const client = await openSsb();
|
||||||
const rootId = await this.getRootId(tribeId);
|
const rootId = await this.getRootId(tribeId);
|
||||||
const keys = tribeCrypto.getKeys(rootId);
|
const keys = cryptoModel.getKeys(rootId);
|
||||||
const gen = tribeCrypto.getGen(rootId);
|
const gen = cryptoModel.getGen(rootId);
|
||||||
if (!keys.length) return;
|
if (!keys.length) return;
|
||||||
const payload = tribeCrypto.buildKeyDistribPayload(rootId, keys, gen);
|
const payload = cryptoModel.buildKeyDistribPayload(rootId, keys, gen);
|
||||||
const others = [...new Set(toMembers)].filter(m => m && m !== client.id);
|
const others = [...new Set(toMembers)].filter(m => m && m !== client.id);
|
||||||
const batch = tribeCrypto.KEY_DISTRIB_BATCH - 1;
|
const batch = cryptoModel.KEY_DISTRIB_BATCH - 1;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
do {
|
do {
|
||||||
const recps = [client.id, ...others.slice(i, i + batch)];
|
const recps = [client.id, ...others.slice(i, i + batch)];
|
||||||
|
|
@ -912,10 +912,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
|
|
||||||
async rotateTribeKey(tribeId, remainingMembers) {
|
async rotateTribeKey(tribeId, remainingMembers) {
|
||||||
const rootId = await this.getRootId(tribeId);
|
const rootId = await this.getRootId(tribeId);
|
||||||
const oldKey = tribeCrypto.getKey(rootId);
|
const oldKey = cryptoModel.getKey(rootId);
|
||||||
if (!oldKey) return;
|
if (!oldKey) return;
|
||||||
const newKey = tribeCrypto.generateTribeKey();
|
const newKey = cryptoModel.generateKey();
|
||||||
tribeCrypto.addNewKey(rootId, newKey);
|
cryptoModel.addNewKey(rootId, newKey);
|
||||||
if (Array.isArray(remainingMembers) && remainingMembers.length > 0) {
|
if (Array.isArray(remainingMembers) && remainingMembers.length > 0) {
|
||||||
await this.distributeTribeKey(tribeId, remainingMembers).catch(() => {});
|
await this.distributeTribeKey(tribeId, remainingMembers).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
@ -928,11 +928,11 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
try { tribe = await this.getTribeById(tribeId); } catch (_) { return; }
|
try { tribe = await this.getTribeById(tribeId); } catch (_) { return; }
|
||||||
if (!tribe || tribe.author !== userId) return;
|
if (!tribe || tribe.author !== userId) return;
|
||||||
const rootId = await this.getRootId(tribeId);
|
const rootId = await this.getRootId(tribeId);
|
||||||
if (!tribeCrypto.getKey(rootId)) return;
|
if (!cryptoModel.getKey(rootId)) return;
|
||||||
const others = (tribe.members || []).filter(m => m !== userId);
|
const others = (tribe.members || []).filter(m => m !== userId);
|
||||||
const { distribs } = await readKeyDistribs();
|
const { distribs } = await readKeyDistribs();
|
||||||
const distributed = new Set();
|
const distributed = new Set();
|
||||||
const currentGen = tribeCrypto.getGen(rootId);
|
const currentGen = cryptoModel.getGen(rootId);
|
||||||
const holdersOfCurrent = new Set();
|
const holdersOfCurrent = new Set();
|
||||||
for (const { m, dec } of distribs) {
|
for (const { m, dec } of distribs) {
|
||||||
if (m.value.author !== userId) continue;
|
if (m.value.author !== userId) continue;
|
||||||
|
|
@ -968,7 +968,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
entries.sort((a, b) => b.generation - a.generation);
|
entries.sort((a, b) => b.generation - a.generation);
|
||||||
const top = entries[0];
|
const top = entries[0];
|
||||||
if (top && Array.isArray(top.keys) && top.keys.length) {
|
if (top && Array.isArray(top.keys) && top.keys.length) {
|
||||||
tribeCrypto.mergeKeys(rootId, top.keys, top.generation);
|
cryptoModel.mergeKeys(rootId, top.keys, top.generation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -978,7 +978,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
const me = client.id;
|
const me = client.id;
|
||||||
let tribe;
|
let tribe;
|
||||||
try { tribe = await this.getTribeById(tribeId); } catch (_) { return; }
|
try { tribe = await this.getTribeById(tribeId); } catch (_) { return; }
|
||||||
const fpIdx = tribeCrypto.buildFingerprintIndex();
|
const fpIdx = cryptoModel.buildFingerprintIndex();
|
||||||
const rootId = await this.getRootId(tribeId).catch(() => tribeId);
|
const rootId = await this.getRootId(tribeId).catch(() => tribeId);
|
||||||
const tribeChainIds = await this.getChainIds(tribeId).catch(() => [tribeId]);
|
const tribeChainIds = await this.getChainIds(tribeId).catch(() => [tribeId]);
|
||||||
const tribeRootSet = new Set([rootId]);
|
const tribeRootSet = new Set([rootId]);
|
||||||
|
|
@ -1006,15 +1006,15 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
if (myInviteMsgKeys.has(c.target)) discovered.add(v.author);
|
if (myInviteMsgKeys.has(c.target)) discovered.add(v.author);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys);
|
const dec = cryptoModel.tryUnboxKeyDistrib(c, config.keys, ssbKeys);
|
||||||
if (dec && tribeRootSet.has(dec.rootId)) {
|
if (dec && tribeRootSet.has(dec.rootId)) {
|
||||||
const recps = Array.isArray(dec.recps) ? dec.recps : [];
|
const recps = Array.isArray(dec.recps) ? dec.recps : [];
|
||||||
for (const r of recps) discovered.add(r);
|
for (const r of recps) discovered.add(r);
|
||||||
if (v.author) discovered.add(v.author);
|
if (v.author) discovered.add(v.author);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (tribeCrypto.isTribeMsg(c)) {
|
if (cryptoModel.isSealedMsg(c)) {
|
||||||
const r = tribeCrypto.unwrapMsg(c, fpIdx);
|
const r = cryptoModel.unwrapMsg(c, fpIdx);
|
||||||
if (!r || !r.body) continue;
|
if (!r || !r.body) continue;
|
||||||
if (r.body.k !== 'tribe') continue;
|
if (r.body.k !== 'tribe') continue;
|
||||||
if (!tribeChainSet.has(m.key) && !tribeChainSet.has(r.body.replaces || '') && r.rootId !== rootId) continue;
|
if (!tribeChainSet.has(m.key) && !tribeChainSet.has(r.body.replaces || '') && r.rootId !== rootId) continue;
|
||||||
|
|
@ -1072,7 +1072,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
while (idx.child.has(tipId)) tipId = idx.child.get(tipId);
|
while (idx.child.has(tipId)) tipId = idx.child.get(tipId);
|
||||||
let tribe;
|
let tribe;
|
||||||
try { tribe = await this.getTribeById(tribeId); } catch (_) { tribe = null; }
|
try { tribe = await this.getTribeById(tribeId); } catch (_) { tribe = null; }
|
||||||
const isPrivate = tribe ? !!tribe.isAnonymous : !!tribeCrypto.getKey(rootId);
|
const isPrivate = tribe ? !!tribe.isAnonymous : !!cryptoModel.getKey(rootId);
|
||||||
let result;
|
let result;
|
||||||
if (isPrivate) {
|
if (isPrivate) {
|
||||||
result = await wrapAndPublish(rootId, { k: 'tombstone', rootId, target: tipId, author: userId, deletedAt: new Date().toISOString() });
|
result = await wrapAndPublish(rootId, { k: 'tombstone', rootId, target: tipId, author: userId, deletedAt: new Date().toISOString() });
|
||||||
|
|
@ -1087,13 +1087,13 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
async pruneOrphanKeys() {
|
async pruneOrphanKeys() {
|
||||||
if (!tribeCrypto || typeof tribeCrypto.getAllRootIds !== 'function') return 0;
|
if (!cryptoModel || typeof cryptoModel.getAllRootIds !== 'function') return 0;
|
||||||
const idx = await buildTribeIndex();
|
const idx = await buildTribeIndex();
|
||||||
const all = tribeCrypto.getAllRootIds();
|
const all = cryptoModel.getAllRootIds();
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
for (const rid of all) {
|
for (const rid of all) {
|
||||||
if (idx.effectivelyTombstoned.has(rid)) {
|
if (idx.effectivelyTombstoned.has(rid)) {
|
||||||
try { tribeCrypto.dropKey(rid); removed++; } catch (_) {}
|
try { cryptoModel.dropKey(rid); removed++; } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return removed;
|
return removed;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require("../server/node_modules/pull-stream");
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const opinionRules = require("../backend/opinion_categories");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { dedupeBy, norm, versionGroups } = require('../backend/dedupe');
|
const { dedupeBy, norm, versionGroups } = require('../backend/dedupe');
|
||||||
|
|
@ -171,7 +172,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (!oldMsg || oldMsg.content?.type !== "video") throw new Error("Video not found");
|
if (!oldMsg || oldMsg.content?.type !== "video") throw new Error("Video not found");
|
||||||
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
if (oldMsg.content.author !== userId) throw new Error("Not the author");
|
||||||
const aggV = await this.getVideoById(id, userId);
|
const aggV = await this.getVideoById(id, userId);
|
||||||
if (aggV && Object.keys(aggV.opinions || {}).some(k => (aggV.opinions[k] || 0) > 0)) throw new Error("Cannot edit video after it has received opinions.");
|
opinionRules.assertMediaKept(aggV, !!blobMarkdown);
|
||||||
|
|
||||||
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags);
|
||||||
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const pull = require('../server/node_modules/pull-stream');
|
const pull = require('../server/node_modules/pull-stream');
|
||||||
|
const opinionRules = require('../backend/opinion_categories');
|
||||||
const moment = require('../server/node_modules/moment');
|
const moment = require('../server/node_modules/moment');
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { getConfig } = require('../configs/config-manager.js');
|
const { getConfig } = require('../configs/config-manager.js');
|
||||||
|
|
@ -15,6 +16,10 @@ const isVoteLocked = (v) => !!v && (
|
||||||
|| Object.values(v.opinions || {}).some(n => (Number(n) || 0) > 0)
|
|| Object.values(v.opinions || {}).some(n => (Number(n) || 0) > 0)
|
||||||
|| (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t))))
|
|| (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t))))
|
||||||
);
|
);
|
||||||
|
const isVoteEditLocked = (v) => !!v && (
|
||||||
|
(parseInt(v.totalVotes || 0, 10) || 0) > 0
|
||||||
|
|| (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t))))
|
||||||
|
);
|
||||||
const lockedError = () => Object.assign(new Error('Vote locked'), { code: 'VOTE_LOCKED' });
|
const lockedError = () => Object.assign(new Error('Vote locked'), { code: 'VOTE_LOCKED' });
|
||||||
|
|
||||||
module.exports = ({ cooler }) => {
|
module.exports = ({ cooler }) => {
|
||||||
|
|
@ -151,6 +156,7 @@ module.exports = ({ cooler }) => {
|
||||||
id: best.key,
|
id: best.key,
|
||||||
latestId: best.key,
|
latestId: best.key,
|
||||||
...best.content,
|
...best.content,
|
||||||
|
rootId: root,
|
||||||
votes: votesMap,
|
votes: votesMap,
|
||||||
voters,
|
voters,
|
||||||
totalVotes,
|
totalVotes,
|
||||||
|
|
@ -253,7 +259,7 @@ module.exports = ({ cooler }) => {
|
||||||
if (c.createdBy !== userId) throw new Error('Not the author');
|
if (c.createdBy !== userId) throw new Error('Not the author');
|
||||||
const agg = await this.getVoteById(id);
|
const agg = await this.getVoteById(id);
|
||||||
const aggTotalVotes = agg ? (parseInt(agg.totalVotes || 0, 10) || 0) : 0;
|
const aggTotalVotes = agg ? (parseInt(agg.totalVotes || 0, 10) || 0) : 0;
|
||||||
if (isVoteLocked(c) || isVoteLocked(agg)) throw lockedError();
|
if (isVoteEditLocked(c) || isVoteEditLocked(agg)) throw lockedError();
|
||||||
|
|
||||||
let newDeadline = c.deadline;
|
let newDeadline = c.deadline;
|
||||||
if (deadline != null && deadline !== '') {
|
if (deadline != null && deadline !== '') {
|
||||||
|
|
@ -276,6 +282,9 @@ module.exports = ({ cooler }) => {
|
||||||
options.some((o, i) => o !== newOptions[i])
|
options.some((o, i) => o !== newOptions[i])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const questionChanged = question !== undefined && String(question) !== String(c.question || '');
|
||||||
|
opinionRules.assertMediaKept(agg && Object.keys(agg.opinions || {}).length ? agg : c, questionChanged || optionsChanged);
|
||||||
|
|
||||||
if (optionsChanged) {
|
if (optionsChanged) {
|
||||||
if (aggTotalVotes > 0) {
|
if (aggTotalVotes > 0) {
|
||||||
throw new Error('Cannot change options after voting has started');
|
throw new Error('Cannot change options after voting has started');
|
||||||
|
|
@ -387,3 +396,4 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
module.exports.isVoteLocked = isVoteLocked;
|
module.exports.isVoteLocked = isVoteLocked;
|
||||||
|
module.exports.isVoteEditLocked = isVoteEditLocked;
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const normalizePolicy = (v) => {
|
||||||
return EDIT_POLICIES.includes(s) ? s : 'open';
|
return EDIT_POLICIES.includes(s) ? s : 'open';
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
module.exports = ({ cooler, cryptoModel = null, tribesModel = null }) => {
|
||||||
let ssb;
|
let ssb;
|
||||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||||
|
|
||||||
|
|
@ -78,9 +78,9 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tribeKeyFor = async (tribeId) => {
|
const tribeKeyFor = async (tribeId) => {
|
||||||
if (!tribeCrypto || !tribeId) return null;
|
if (!cryptoModel || !tribeId) return null;
|
||||||
const rootId = await tribeRootId(tribeId);
|
const rootId = await tribeRootId(tribeId);
|
||||||
return tribeCrypto.getKey(rootId) || null;
|
return cryptoModel.getKey(rootId) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeBody = async (ssbClient, body, tribeId) => {
|
const storeBody = async (ssbClient, body, tribeId) => {
|
||||||
|
|
@ -90,7 +90,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
if (tribeId) {
|
if (tribeId) {
|
||||||
const key = await tribeKeyFor(tribeId);
|
const key = await tribeKeyFor(tribeId);
|
||||||
if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page');
|
if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page');
|
||||||
payload = JSON.stringify(tribeCrypto.encryptContent({ body: text }, [key], true));
|
payload = JSON.stringify(cryptoModel.encryptContent({ body: text }, [key], true));
|
||||||
}
|
}
|
||||||
const ref = await addBlob(ssbClient, Buffer.from(payload, 'utf8'));
|
const ref = await addBlob(ssbClient, Buffer.from(payload, 'utf8'));
|
||||||
bodyCache.set(ref, text);
|
bodyCache.set(ref, text);
|
||||||
|
|
@ -106,7 +106,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
if (c.tribeId) {
|
if (c.tribeId) {
|
||||||
try {
|
try {
|
||||||
const key = await tribeKeyFor(c.tribeId);
|
const key = await tribeKeyFor(c.tribeId);
|
||||||
const r = tribeCrypto.decryptContent(JSON.parse(text), key ? [[key]] : []);
|
const r = cryptoModel.decryptContent(JSON.parse(text), key ? [[key]] : []);
|
||||||
text = r && !r._undecryptable && typeof r.body === 'string' ? r.body : '';
|
text = r && !r._undecryptable && typeof r.body === 'string' ? r.body : '';
|
||||||
} catch (_) { text = ''; }
|
} catch (_) { text = ''; }
|
||||||
}
|
}
|
||||||
|
|
@ -116,15 +116,15 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
|
|
||||||
const readEntries = async (ssbClient) => {
|
const readEntries = async (ssbClient) => {
|
||||||
const msgs = await readTyped(ssbClient, [WIKI_TYPE, 'tombstone', ENVELOPE_TYPE], { limit: logLimit });
|
const msgs = await readTyped(ssbClient, [WIKI_TYPE, 'tombstone', ENVELOPE_TYPE], { limit: logLimit });
|
||||||
const fpIdx = tribeCrypto && typeof tribeCrypto.buildFingerprintIndex === 'function' ? tribeCrypto.buildFingerprintIndex() : null;
|
const fpIdx = cryptoModel && typeof cryptoModel.buildFingerprintIndex === 'function' ? cryptoModel.buildFingerprintIndex() : null;
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const m of msgs) {
|
for (const m of msgs) {
|
||||||
const v = m && m.value;
|
const v = m && m.value;
|
||||||
const c = v && v.content;
|
const c = v && v.content;
|
||||||
if (!c || typeof c !== 'object') continue;
|
if (!c || typeof c !== 'object') continue;
|
||||||
if (c.type === WIKI_TYPE || c.type === 'tombstone') { out.push(m); continue; }
|
if (c.type === WIKI_TYPE || c.type === 'tombstone') { out.push(m); continue; }
|
||||||
if (!fpIdx || !tribeCrypto.isTribeMsg(c)) continue;
|
if (!fpIdx || !cryptoModel.isSealedMsg(c)) continue;
|
||||||
const r = tribeCrypto.unwrapMsg(c, fpIdx);
|
const r = cryptoModel.unwrapMsg(c, fpIdx);
|
||||||
if (!r || !r.body || typeof r.body !== 'object') continue;
|
if (!r || !r.body || typeof r.body !== 'object') continue;
|
||||||
const body = r.body;
|
const body = r.body;
|
||||||
if (body.k === WIKI_TYPE) {
|
if (body.k === WIKI_TYPE) {
|
||||||
|
|
@ -298,7 +298,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
if (content.tribeId) {
|
if (content.tribeId) {
|
||||||
const key = await tribeKeyFor(content.tribeId);
|
const key = await tribeKeyFor(content.tribeId);
|
||||||
if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page');
|
if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page');
|
||||||
msg = tribeCrypto.wrapMsg({ k: WIKI_TYPE, ...content }, key);
|
msg = cryptoModel.wrapMsg({ k: WIKI_TYPE, ...content }, key);
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res)));
|
return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res)));
|
||||||
};
|
};
|
||||||
|
|
@ -407,7 +407,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => {
|
||||||
if (page.tribeId) {
|
if (page.tribeId) {
|
||||||
const key = await tribeKeyFor(page.tribeId);
|
const key = await tribeKeyFor(page.tribeId);
|
||||||
if (!key) throw new Error('Missing tribe key — cannot delete encrypted wiki page');
|
if (!key) throw new Error('Missing tribe key — cannot delete encrypted wiki page');
|
||||||
msg = tribeCrypto.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key);
|
msg = cryptoModel.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key);
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res)));
|
return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res)));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
499
src/patches/patch-node-modules.js
Normal file
499
src/patches/patch-node-modules.js
Normal file
|
|
@ -0,0 +1,499 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const verbose = process.argv.includes('--verbose') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
||||||
|
const log = (msg) => { if (verbose) console.log(`[OASIS] [PATCH] ${msg}`); };
|
||||||
|
|
||||||
|
// === Patch ssb-ref ===
|
||||||
|
const ssbRefPath = path.resolve(__dirname, '../server/node_modules/ssb-ref/index.js');
|
||||||
|
if (fs.existsSync(ssbRefPath)) {
|
||||||
|
const data = fs.readFileSync(ssbRefPath, 'utf8');
|
||||||
|
const alreadyClean = /exports\.parseAddress\s*=\s*parseAddress/.test(data);
|
||||||
|
if (!alreadyClean) {
|
||||||
|
const patched = data.replace(
|
||||||
|
/exports\.parseAddress\s*=\s*deprecate\(\s*['"][^'"]*['"]\s*,\s*parseAddress\s*\)/,
|
||||||
|
'exports.parseAddress = parseAddress'
|
||||||
|
);
|
||||||
|
if (patched !== data) {
|
||||||
|
fs.writeFileSync(ssbRefPath, patched);
|
||||||
|
log('Patched ssb-ref to remove deprecated usage of parseAddress');
|
||||||
|
} else {
|
||||||
|
log('ssb-ref patch skipped: unexpected parseAddress export format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-ref patch skipped: file not found at ' + ssbRefPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-blobs ===
|
||||||
|
const ssbBlobsPath = path.resolve(__dirname, '../server/node_modules/ssb-blobs/inject.js');
|
||||||
|
if (fs.existsSync(ssbBlobsPath)) {
|
||||||
|
let data = fs.readFileSync(ssbBlobsPath, 'utf8');
|
||||||
|
|
||||||
|
const marker = 'want: function (id, cb)';
|
||||||
|
const startIndex = data.indexOf(marker);
|
||||||
|
if (data.includes('if (wantCallbacks[id]) registerWant(id);')) {
|
||||||
|
log('ssb-blobs already patched');
|
||||||
|
} else if (startIndex !== -1) {
|
||||||
|
const endIndex = data.indexOf('},', startIndex);
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
const before = data.slice(0, startIndex);
|
||||||
|
const after = data.slice(endIndex + 2);
|
||||||
|
|
||||||
|
const replacement = `
|
||||||
|
want: function (id, cb) {
|
||||||
|
id = toBlobId(id);
|
||||||
|
if (!isBlobId(id)) return cb(new Error('invalid id:' + id));
|
||||||
|
|
||||||
|
if (blobStore.isEmptyHash(id)) return cb(null, true);
|
||||||
|
|
||||||
|
if (wantCallbacks[id]) {
|
||||||
|
if (!Array.isArray(wantCallbacks[id])) wantCallbacks[id] = [];
|
||||||
|
wantCallbacks[id].push(cb);
|
||||||
|
} else {
|
||||||
|
wantCallbacks[id] = [cb];
|
||||||
|
blobStore.size(id, function (err, size) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (size != null) {
|
||||||
|
while (wantCallbacks[id].length) {
|
||||||
|
const fn = wantCallbacks[id].shift();
|
||||||
|
if (typeof fn === 'function') fn(null, true);
|
||||||
|
}
|
||||||
|
delete wantCallbacks[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerId = findPeerWithBlob(id);
|
||||||
|
if (peerId) get(peerId, id);
|
||||||
|
|
||||||
|
if (wantCallbacks[id]) registerWant(id);
|
||||||
|
},`;
|
||||||
|
|
||||||
|
const finalData = before + replacement + after;
|
||||||
|
fs.writeFileSync(ssbBlobsPath, finalData);
|
||||||
|
log('Patched ssb-blobs to fix wantCallbacks handling');
|
||||||
|
} else {
|
||||||
|
log('ssb-blobs patch skipped: end of want function not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-blobs patch skipped: want function not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-db2 (onceWhen leaks its listener when the condition already holds) ===
|
||||||
|
const ssbDb2UtilsPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/utils.js');
|
||||||
|
if (fs.existsSync(ssbDb2UtilsPath)) {
|
||||||
|
const data = fs.readFileSync(ssbDb2UtilsPath, 'utf8');
|
||||||
|
if (!data.includes('answered = true\n cb()\n return false')) {
|
||||||
|
const start = data.indexOf('function onceWhen(obv, filter, cb) {');
|
||||||
|
const end = data.indexOf('\n}\n', start);
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
const replacement = `function onceWhen(obv, filter, cb) {
|
||||||
|
if (!obv) return cb()
|
||||||
|
let answered = false
|
||||||
|
obv((x) => {
|
||||||
|
if (answered) return false
|
||||||
|
if (!filter(x)) return
|
||||||
|
answered = true
|
||||||
|
cb()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}`;
|
||||||
|
fs.writeFileSync(ssbDb2UtilsPath, data.slice(0, start) + replacement + data.slice(end + 2));
|
||||||
|
log('Patched ssb-db2 utils.js so onceWhen releases its listener once answered');
|
||||||
|
} else {
|
||||||
|
log('ssb-db2 patch skipped: onceWhen not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-db2 patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssbDb2PluginPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/indexes/plugin.js');
|
||||||
|
if (fs.existsSync(ssbDb2PluginPath)) {
|
||||||
|
const data = fs.readFileSync(ssbDb2PluginPath, 'utf8');
|
||||||
|
const target = ` this.onRecord = function onRecord(record, isLive, pValue) {`;
|
||||||
|
const flushTarget = ` if (changes > chunkSize) this.flush(thenMaybeReportError)\n else if (isLive) liveFlush(thenMaybeReportError)`;
|
||||||
|
if (!data.includes('lastFlushAt') && data.includes(target) && data.includes(flushTarget)) {
|
||||||
|
fs.writeFileSync(ssbDb2PluginPath, data
|
||||||
|
.replace(target, ` let lastFlushAt = Date.now()\n${target}`)
|
||||||
|
.replace(flushTarget, ` if (changes > chunkSize || Date.now() - lastFlushAt > 1000) {\n lastFlushAt = Date.now()\n this.flush(thenMaybeReportError)\n } else if (isLive) liveFlush(thenMaybeReportError)`));
|
||||||
|
log('Patched ssb-db2 indexes so their progress is saved at least once per second');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-db2 index patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch @xenova/transformers (onnxruntime 1.19 Tensor getter) ===
|
||||||
|
const xenovaTensorPath = path.resolve(__dirname, '../AI/node_modules/@xenova/transformers/src/utils/tensor.js');
|
||||||
|
if (fs.existsSync(xenovaTensorPath)) {
|
||||||
|
let data = fs.readFileSync(xenovaTensorPath, 'utf8');
|
||||||
|
if (!data.includes('this.data = args[0].data')) {
|
||||||
|
const patched = data
|
||||||
|
.replace(
|
||||||
|
" if (args[0] instanceof ONNXTensor) {\n // Create shallow copy\n Object.assign(this, args[0]);\n",
|
||||||
|
" if (args[0] instanceof ONNXTensor) {\n // Create shallow copy\n Object.assign(this, args[0]);\n if (this.data === undefined) this.data = args[0].data;\n"
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
" args[2]\n ));\n }",
|
||||||
|
" args[2]\n ));\n if (this.data === undefined && this.cpuData !== undefined) this.data = this.cpuData;\n }"
|
||||||
|
);
|
||||||
|
if (patched !== data) {
|
||||||
|
fs.writeFileSync(xenovaTensorPath, patched);
|
||||||
|
log('Patched @xenova/transformers tensor.js for onnxruntime 1.19 data getter');
|
||||||
|
} else {
|
||||||
|
log('@xenova/transformers patch skipped: unexpected tensor.js format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('@xenova/transformers patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-conn (the scheduler prefers up-to-date peers that replicate what we follow) ===
|
||||||
|
const connSchedulerPath = path.resolve(__dirname, '../server/node_modules/ssb-conn/lib/conn-scheduler.js');
|
||||||
|
if (fs.existsSync(connSchedulerPath)) {
|
||||||
|
const data = fs.readFileSync(connSchedulerPath, 'utf8');
|
||||||
|
const best = " .z((peers) => typeof this.ssb.oasisPeerRank === 'function' ? peers.sort((a, b) => this.ssb.oasisPeerRank(b) - this.ssb.oasisPeerRank(a)) : peers)\n";
|
||||||
|
const worst = " .z((peers) => typeof this.ssb.oasisPeerRank === 'function' ? peers.sort((a, b) => this.ssb.oasisPeerRank(a) - this.ssb.oasisPeerRank(b)) : peers)\n";
|
||||||
|
const connectAnchor = " .z(sortByCooldownAscending)\n .z(take(freeSlots))";
|
||||||
|
const rotateAnchor = " .z(sortByOldestConnection)\n .z(take(1))";
|
||||||
|
if (data.includes('this.ssb.oasisPeerRank')) {
|
||||||
|
log('ssb-conn scheduler already patched');
|
||||||
|
} else if (data.includes(connectAnchor) && data.includes(rotateAnchor)) {
|
||||||
|
fs.writeFileSync(connSchedulerPath, data
|
||||||
|
.replace(connectAnchor, " .z(sortByCooldownAscending)\n" + best + " .z(take(freeSlots))")
|
||||||
|
.replace(rotateAnchor, " .z(sortByOldestConnection)\n" + worst + " .z(take(1))"));
|
||||||
|
log('Patched ssb-conn scheduler to rank peers by version and shared replication');
|
||||||
|
} else {
|
||||||
|
log('ssb-conn scheduler patch skipped: unexpected conn-scheduler.js format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-conn scheduler patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-conn (connection limits apply to the connections we open, never to the ones others open to us) ===
|
||||||
|
const connHubPath = path.resolve(__dirname, '../server/node_modules/ssb-conn-hub/lib/index.js');
|
||||||
|
if (fs.existsSync(connHubPath) && fs.existsSync(connSchedulerPath)) {
|
||||||
|
const hub = fs.readFileSync(connHubPath, 'utf8');
|
||||||
|
const sched = fs.readFileSync(connSchedulerPath, 'utf8');
|
||||||
|
const hubFrom = 'this._setPeer(address, { ...data, state, disconnect });';
|
||||||
|
const hubTo = 'this._setPeer(address, { ...data, state, disconnect, inbound: !isClient });';
|
||||||
|
const schedFrom = 'const peersUp = query.peersConnected().filter(isDesiredPeer);';
|
||||||
|
const schedTo = 'const peersUp = query.peersConnected().filter(isDesiredPeer).filter((p) => !p[1].inbound);';
|
||||||
|
if (hub.includes(hubTo) && sched.includes(schedTo)) {
|
||||||
|
log('ssb-conn inbound limits already patched');
|
||||||
|
} else if ((hub.includes(hubFrom) || hub.includes(hubTo)) && (sched.includes(schedFrom) || sched.includes(schedTo))) {
|
||||||
|
fs.writeFileSync(connHubPath, hub.replace(hubFrom, hubTo));
|
||||||
|
fs.writeFileSync(connSchedulerPath, sched.replace(schedFrom, schedTo));
|
||||||
|
log('Patched ssb-conn so its connection limits only apply to the connections it opens');
|
||||||
|
} else {
|
||||||
|
log('ssb-conn inbound patch skipped: unexpected format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-conn inbound patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONN_QUOTA = 10;
|
||||||
|
const connQuotaFrom = [4, CONN_QUOTA].map((n) => `this.maintainConnections(${n}, isNotRoom, 'dbAndStaging', null, 2 * HOUR);`);
|
||||||
|
const connQuotaTo = `this.maintainConnections(typeof this.ssb.oasisConnQuota === 'function' ? this.ssb.oasisConnQuota() : ${CONN_QUOTA}, isNotRoom, 'dbAndStaging', null, 2 * HOUR);`;
|
||||||
|
if (fs.existsSync(connSchedulerPath)) {
|
||||||
|
const sched = fs.readFileSync(connSchedulerPath, 'utf8');
|
||||||
|
const from = connQuotaFrom.find((f) => sched.includes(f));
|
||||||
|
if (sched.includes(connQuotaTo)) {
|
||||||
|
log('ssb-conn already takes its outgoing connections from the node settings');
|
||||||
|
} else if (from) {
|
||||||
|
fs.writeFileSync(connSchedulerPath, sched.replace(from, connQuotaTo));
|
||||||
|
log('Patched ssb-conn to take its outgoing connections from the node settings');
|
||||||
|
} else {
|
||||||
|
log('ssb-conn quota patch skipped: unexpected conn-scheduler.js format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-conn quota patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-lan (broadcast address detection throws without a private IPv4) ===
|
||||||
|
const ssbLanPath = path.resolve(__dirname, '../server/node_modules/ssb-lan/lib/index.js');
|
||||||
|
if (fs.existsSync(ssbLanPath)) {
|
||||||
|
const data = fs.readFileSync(ssbLanPath, 'utf8');
|
||||||
|
if (!data.includes("e.family === 'IPv4' && IP.isPrivate(addr)")) {
|
||||||
|
const start = data.indexOf(' getBroadcastIPs() {');
|
||||||
|
const end = data.indexOf('\n }\n', start);
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
const replacement = ` getBroadcastIPs() {
|
||||||
|
if (process.platform === 'ios')
|
||||||
|
return ['255.255.255.255'];
|
||||||
|
try {
|
||||||
|
const details = nonPrivateIP(null, (addr, e) => e.family === 'IPv4' && IP.isPrivate(addr), true);
|
||||||
|
if (!details || details.family !== 'IPv4')
|
||||||
|
return ['255.255.255.255'];
|
||||||
|
const { broadcastAddress } = IP.subnet(details.address, details.netmask);
|
||||||
|
return [broadcastAddress];
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
debug('LAN broadcast address detection failed: %s', err);
|
||||||
|
return ['255.255.255.255'];
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
fs.writeFileSync(ssbLanPath, data.slice(0, start) + replacement + data.slice(end + 6));
|
||||||
|
log('Patched ssb-lan to fall back to the global broadcast address');
|
||||||
|
} else {
|
||||||
|
log('ssb-lan patch skipped: getBroadcastIPs not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-lan patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-box (map leaks the index as libsodium's output format without native bindings) ===
|
||||||
|
const ssbBoxPath = path.resolve(__dirname, '../server/node_modules/ssb-box/format.js');
|
||||||
|
if (fs.existsSync(ssbBoxPath)) {
|
||||||
|
const data = fs.readFileSync(ssbBoxPath, 'utf8');
|
||||||
|
const target = '.map(sodium.crypto_sign_ed25519_pk_to_curve25519);';
|
||||||
|
if (data.includes(target)) {
|
||||||
|
fs.writeFileSync(ssbBoxPath, data.replace(target, '.map((pk) => sodium.crypto_sign_ed25519_pk_to_curve25519(pk));'));
|
||||||
|
log('Patched ssb-box so recipient keys are converted one argument at a time');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-box patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch multiserver (voice frames are small and frequent: send them without Nagle's delay) ===
|
||||||
|
const msNetPath = path.resolve(__dirname, '../server/node_modules/multiserver/plugins/net.js');
|
||||||
|
if (fs.existsSync(msNetPath)) {
|
||||||
|
let data = fs.readFileSync(msNetPath, 'utf8');
|
||||||
|
if (data.includes('setNoDelay(true)')) {
|
||||||
|
log('multiserver already patched');
|
||||||
|
} else {
|
||||||
|
const serverTarget = ` function connectionListener(stream) {
|
||||||
|
onConnection(toDuplex(stream))`;
|
||||||
|
const clientTarget = ` .on('connect', function onConnect() {
|
||||||
|
if (started) return
|
||||||
|
started = true
|
||||||
|
cb(null, toDuplex(stream))`;
|
||||||
|
if (data.includes(serverTarget) && data.includes(clientTarget)) {
|
||||||
|
data = data
|
||||||
|
.replace(serverTarget, ` function connectionListener(stream) {
|
||||||
|
try { stream.setNoDelay(true) } catch (_) {}
|
||||||
|
onConnection(toDuplex(stream))`)
|
||||||
|
.replace(clientTarget, ` .on('connect', function onConnect() {
|
||||||
|
if (started) return
|
||||||
|
started = true
|
||||||
|
try { stream.setNoDelay(true) } catch (_) {}
|
||||||
|
cb(null, toDuplex(stream))`);
|
||||||
|
fs.writeFileSync(msNetPath, data);
|
||||||
|
log('Patched multiserver net plugin to disable Nagle on every connection');
|
||||||
|
} else {
|
||||||
|
log('multiserver patch skipped: unexpected net plugin layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('multiserver patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch hyperaxe (only strings and nodes become children; raw HTML only through a private symbol) ===
|
||||||
|
const hyperaxePath = path.resolve(__dirname, '../server/node_modules/hyperaxe/factory.js');
|
||||||
|
if (fs.existsSync(hyperaxePath)) {
|
||||||
|
let data = fs.readFileSync(hyperaxePath, 'utf8');
|
||||||
|
if (data.includes('oasis.rawHtml')) {
|
||||||
|
log('hyperaxe already patched');
|
||||||
|
} else {
|
||||||
|
const target = ` return function (props) {
|
||||||
|
return isObject(props)
|
||||||
|
? fn(tag, props, sliceKids(arguments, 1))
|
||||||
|
: fn(tag, sliceKids(arguments))
|
||||||
|
}`;
|
||||||
|
if (data.includes(target)) {
|
||||||
|
data = data.replace(target, ` return function (props) {
|
||||||
|
return isObject(props)
|
||||||
|
? fn(tag, cleanProps(props), scrubKids(sliceKids(arguments, 1)))
|
||||||
|
: fn(tag, scrubKids(sliceKids(arguments)))
|
||||||
|
}`);
|
||||||
|
data += `
|
||||||
|
const RAW_HTML = Symbol.for('oasis.rawHtml')
|
||||||
|
function isNodeLike (v) {
|
||||||
|
return !!(v && v.nodeName && v.nodeType)
|
||||||
|
}
|
||||||
|
function scrubKids (arr) {
|
||||||
|
return arr.map(function (v) {
|
||||||
|
if (Array.isArray(v)) return scrubKids(v)
|
||||||
|
if (v !== null && typeof v === 'object' && !isNodeLike(v) && !(v instanceof Date) && !(v instanceof RegExp)) return null
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function cleanProps (props) {
|
||||||
|
if (isNodeLike(props)) return props
|
||||||
|
const out = {}
|
||||||
|
for (const k of Object.keys(props)) if (k !== 'innerHTML') out[k] = props[k]
|
||||||
|
if (typeof props[RAW_HTML] === 'string') out.innerHTML = props[RAW_HTML]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(hyperaxePath, data);
|
||||||
|
log('Patched hyperaxe so peer objects never become attributes or raw HTML');
|
||||||
|
} else {
|
||||||
|
log('hyperaxe patch skipped: unexpected factory layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('hyperaxe patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch html-element (standard attributes missing from its list are written instead of dropped) ===
|
||||||
|
const htmlAttributesPath = path.resolve(__dirname, '../server/node_modules/html-element/html-attributes.js');
|
||||||
|
if (fs.existsSync(htmlAttributesPath)) {
|
||||||
|
const data = fs.readFileSync(htmlAttributesPath, 'utf8');
|
||||||
|
const anchor = 'function isStandardAttribute(attrName, tagName) {';
|
||||||
|
const marker = 'OASIS_EXTRA_ATTRIBUTES';
|
||||||
|
if (data.includes(marker)) {
|
||||||
|
log('html-element attributes already patched');
|
||||||
|
} else if (data.includes(anchor)) {
|
||||||
|
const extra = `var OASIS_EXTRA_ATTRIBUTES = {
|
||||||
|
'formmethod': ['input', 'button'],
|
||||||
|
'formenctype': ['input', 'button'],
|
||||||
|
'formnovalidate': ['input', 'button'],
|
||||||
|
'formtarget': ['input', 'button'],
|
||||||
|
'minlength': ['input', 'textarea'],
|
||||||
|
'inputmode': 'GLOBAL',
|
||||||
|
'capture': ['input'],
|
||||||
|
'loading': ['img', 'iframe'],
|
||||||
|
'decoding': ['img'],
|
||||||
|
'crossorigin': ['audio', 'img', 'link', 'script', 'video'],
|
||||||
|
'referrerpolicy': ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
||||||
|
'playsinline': ['video'],
|
||||||
|
'role': 'GLOBAL',
|
||||||
|
'label': ['option', 'optgroup', 'track']
|
||||||
|
};
|
||||||
|
Object.keys(OASIS_EXTRA_ATTRIBUTES).forEach(function (name) {
|
||||||
|
var tags = OASIS_EXTRA_ATTRIBUTES[name];
|
||||||
|
var current = HTML_ATTRIBUTES[name];
|
||||||
|
if (tags === 'GLOBAL' || current === 'GLOBAL') { HTML_ATTRIBUTES[name] = 'GLOBAL'; return; }
|
||||||
|
var set = current instanceof Set ? current : new Set();
|
||||||
|
tags.forEach(function (t) { set.add(t); });
|
||||||
|
HTML_ATTRIBUTES[name] = set;
|
||||||
|
});
|
||||||
|
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(htmlAttributesPath, data.replace(anchor, extra + anchor));
|
||||||
|
log('Patched html-element so standard attributes such as formmethod, minlength and loading are written');
|
||||||
|
} else {
|
||||||
|
log('html-element attributes patch skipped: unexpected html-attributes.js format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('html-element attributes patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-invite (local read of how many uses each invite has and how many are spent) ===
|
||||||
|
const ssbInvitePath = path.resolve(__dirname, '../server/node_modules/ssb-invite/index.js');
|
||||||
|
const ssbInviteManifestPath = path.resolve(__dirname, '../server/node_modules/ssb-invite/manifest.json');
|
||||||
|
if (fs.existsSync(ssbInvitePath) && fs.existsSync(ssbInviteManifestPath)) {
|
||||||
|
const data = fs.readFileSync(ssbInvitePath, 'utf8');
|
||||||
|
const anchor = " }, 'string')\n }\n }\n}";
|
||||||
|
if (data.includes('codesDB.createReadStream')) {
|
||||||
|
log('ssb-invite already patched');
|
||||||
|
} else if (data.includes(anchor)) {
|
||||||
|
const list = ` }, 'string'),
|
||||||
|
list: function (cb) {
|
||||||
|
const out = []
|
||||||
|
let done = false
|
||||||
|
const finish = (err) => { if (done) return; done = true; err ? cb(err) : cb(null, out) }
|
||||||
|
codesDB.createReadStream()
|
||||||
|
.on('data', (d) => { const v = d.value || {}; out.push({ id: d.key, total: +v.total || 0, used: +v.used || 0 }) })
|
||||||
|
.on('error', finish)
|
||||||
|
.on('end', () => finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
fs.writeFileSync(ssbInvitePath, data.replace(anchor, list));
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(ssbInviteManifestPath, 'utf8'));
|
||||||
|
manifest.list = 'async';
|
||||||
|
fs.writeFileSync(ssbInviteManifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
log('Patched ssb-invite with a local list of invite uses');
|
||||||
|
} else {
|
||||||
|
log('ssb-invite patch skipped: unexpected index.js layout');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-invite patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch on-wakeup (a busy event loop is not a wake-up: only time the machine spent suspended counts) ===
|
||||||
|
const onWakeupPath = path.resolve(__dirname, '../server/node_modules/on-wakeup/index.js');
|
||||||
|
if (fs.existsSync(onWakeupPath)) {
|
||||||
|
const data = fs.readFileSync(onWakeupPath, 'utf8');
|
||||||
|
const tickFrom = ` var t = Date.now()
|
||||||
|
if (lastSleepCheck && (t - lastSleepCheck) > SLEEP_CHECK_INTERVAL*NUM_MISSABLE_INTERVALS)
|
||||||
|
emitter.emit('wakeup') // missed NUM_MISSABLE_INTERVALS checks, let's run the callbacks
|
||||||
|
lastSleepCheck = t`;
|
||||||
|
const tickTo = ` var t = Date.now()
|
||||||
|
var hr = process.hrtime()
|
||||||
|
var awake = hr[0] * 1e3 + hr[1] / 1e6
|
||||||
|
var gap = t - lastSleepCheck
|
||||||
|
var suspended = process.platform === 'win32' ? gap : gap - (awake - lastAwake)
|
||||||
|
if (lastSleepCheck && gap > SLEEP_CHECK_INTERVAL*NUM_MISSABLE_INTERVALS && suspended > SLEEP_CHECK_INTERVAL*(NUM_MISSABLE_INTERVALS-1))
|
||||||
|
emitter.emit('wakeup')
|
||||||
|
lastSleepCheck = t
|
||||||
|
lastAwake = awake`;
|
||||||
|
const varsFrom = 'var lastSleepCheck = false\n';
|
||||||
|
const varsTo = 'var lastSleepCheck = false\nvar lastAwake = 0\n';
|
||||||
|
if (data.includes('var lastAwake = 0')) {
|
||||||
|
log('on-wakeup already patched');
|
||||||
|
} else if (data.includes(tickFrom) && data.includes(varsFrom)) {
|
||||||
|
fs.writeFileSync(onWakeupPath, data.replace(varsFrom, varsTo).replace(tickFrom, tickTo));
|
||||||
|
log('Patched on-wakeup so a long busy stretch is not taken for a wake-up');
|
||||||
|
} else {
|
||||||
|
log('on-wakeup patch skipped: unexpected index.js layout');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('on-wakeup patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-db2 (createHistoryStream returns a feed in arrival order, which is its sequence order, not by timestamp) ===
|
||||||
|
const historyStreamPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/compat/history-stream.js');
|
||||||
|
if (fs.existsSync(historyStreamPath)) {
|
||||||
|
const data = fs.readFileSync(historyStreamPath, 'utf8');
|
||||||
|
const importFrom = "const { author, and, gte, where, batch, toPullStream } = require('../operators')";
|
||||||
|
const importTo = "const { author, and, gte, where, batch, sortByArrival, toPullStream } = require('../operators')";
|
||||||
|
const queryFrom = ' where(query),\n';
|
||||||
|
const queryTo = ' where(query),\n sortByArrival(),\n';
|
||||||
|
if (data.includes(importTo)) {
|
||||||
|
log('ssb-db2 history stream already patched');
|
||||||
|
} else if (data.includes(importFrom) && data.split(queryFrom).length === 3) {
|
||||||
|
fs.writeFileSync(historyStreamPath, data.replace(importFrom, importTo).split(queryFrom).join(queryTo));
|
||||||
|
log('Patched ssb-db2 so createHistoryStream keeps sequence order');
|
||||||
|
} else {
|
||||||
|
log('ssb-db2 history stream patch skipped: unexpected history-stream.js layout');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-db2 history stream patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Patch ssb-invite-client (a pub that already follows us is not a failed invite: keep the pub and finish joining) ===
|
||||||
|
const inviteClientPath = path.resolve(__dirname, '../server/node_modules/ssb-invite-client/lib/index.js');
|
||||||
|
if (fs.existsSync(inviteClientPath)) {
|
||||||
|
const data = fs.readFileSync(inviteClientPath, 'utf8');
|
||||||
|
const useFrom = ` const [e2] = await run(rpc.invite.use)({ feed: this.ssb.id });
|
||||||
|
if (e2)
|
||||||
|
return cb(clarify(e2, 'Invite not accepted by the pub'));
|
||||||
|
if (shouldPublish) {`;
|
||||||
|
const useTo = ` const [e2] = await run(rpc.invite.use)({ feed: this.ssb.id });
|
||||||
|
const knownToPub = !!e2 && /already following/i.test(String((e2 && e2.message) || e2));
|
||||||
|
if (e2 && !knownToPub)
|
||||||
|
return cb(clarify(e2, 'Invite not accepted by the pub'));
|
||||||
|
let followsPub = false;
|
||||||
|
if (knownToPub && this.ssb.friends && typeof this.ssb.friends.isFollowing === 'function') {
|
||||||
|
const [, yes] = await run(this.ssb.friends.isFollowing)({ source: this.ssb.id, dest: parsed.key });
|
||||||
|
followsPub = !!yes;
|
||||||
|
}
|
||||||
|
if (shouldPublish && !followsPub) {`;
|
||||||
|
if (data.includes('knownToPub')) {
|
||||||
|
log('ssb-invite-client already patched');
|
||||||
|
} else if (data.includes(useFrom)) {
|
||||||
|
fs.writeFileSync(inviteClientPath, data.replace(useFrom, useTo));
|
||||||
|
log('Patched ssb-invite-client so a pub that already follows us still gets joined');
|
||||||
|
} else {
|
||||||
|
log('ssb-invite-client patch skipped: unexpected lib/index.js layout');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('ssb-invite-client patch skipped: file not found');
|
||||||
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ const Server = SecretStack({ caps })
|
||||||
.use(require('ssb-unix-socket'))
|
.use(require('ssb-unix-socket'))
|
||||||
.use(require('ssb-no-auth'))
|
.use(require('ssb-no-auth'))
|
||||||
.use(require('./snapshot_plugin'))
|
.use(require('./snapshot_plugin'))
|
||||||
.use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); } } catch (_) {} return pm; })())
|
.use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); pm.setScreenFactory(require('./phone_screen_android')); } } catch (_) {} return pm; })())
|
||||||
.use(require('./network_pause'));
|
.use(require('./network_pause'));
|
||||||
|
|
||||||
if (!config.pub) {
|
if (!config.pub) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { connectionsOf } = require('../configs/config-manager');
|
||||||
|
|
||||||
|
const QUOTA_TTL_MS = 5000;
|
||||||
|
|
||||||
const readOasisConfig = () => {
|
const readOasisConfig = () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,6 +18,12 @@ module.exports = {
|
||||||
manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' },
|
manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' },
|
||||||
init(server, config) {
|
init(server, config) {
|
||||||
server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1';
|
server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1';
|
||||||
|
let quota = connectionsOf(readOasisConfig().connections);
|
||||||
|
let quotaAt = Date.now();
|
||||||
|
server.oasisConnQuota = () => {
|
||||||
|
if (Date.now() - quotaAt > QUOTA_TTL_MS) { quota = connectionsOf(readOasisConfig().connections); quotaAt = Date.now(); }
|
||||||
|
return quota;
|
||||||
|
};
|
||||||
const refused = () => new Error('network paused');
|
const refused = () => new Error('network paused');
|
||||||
|
|
||||||
if (server.auth && typeof server.auth.hook === 'function') {
|
if (server.auth && typeof server.auth.hook === 'function') {
|
||||||
|
|
@ -46,18 +55,26 @@ module.exports = {
|
||||||
const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/);
|
const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/);
|
||||||
return canonicalKey((ev && ev.key) || (m ? m[1] : ''));
|
return canonicalKey((ev && ev.key) || (m ? m[1] : ''));
|
||||||
};
|
};
|
||||||
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
const versionParts = (v) => String(v || '').split('.').slice(0, 3).map(n => parseInt(n, 10) || 0);
|
||||||
const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })();
|
const compareVersions = (a, b) => {
|
||||||
const upToDate = (v) => {
|
const x = versionParts(a), y = versionParts(b);
|
||||||
const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion);
|
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
|
||||||
return a1 > a2 || (a1 === a2 && b1 >= b2);
|
return 0;
|
||||||
|
};
|
||||||
|
const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })();
|
||||||
|
const rank = { versions: new Map(), graph: {}, newest: ownVersion };
|
||||||
|
const versionTier = (v, total) => {
|
||||||
|
if (!v) return total > 0 ? 0 : 2;
|
||||||
|
if (compareVersions(v, rank.newest) >= 0) return 4;
|
||||||
|
const [a1, b1] = versionParts(v), [a2, b2] = versionParts(rank.newest);
|
||||||
|
return a1 === a2 && b1 === b2 ? 3 : 1;
|
||||||
};
|
};
|
||||||
const rank = { versions: new Map(), graph: {} };
|
|
||||||
const refreshRank = () => {
|
const refreshRank = () => {
|
||||||
const pull = require('pull-stream');
|
const pull = require('pull-stream');
|
||||||
const latest = new Map();
|
const latest = new Map();
|
||||||
const done = () => {
|
const done = () => {
|
||||||
rank.versions = new Map([...latest].map(([k, m]) => [k, m.version]));
|
rank.versions = new Map([...latest].map(([k, m]) => [k, m.version]));
|
||||||
|
rank.newest = [...rank.versions.values()].reduce((n, v) => (compareVersions(v, n) > 0 ? v : n), ownVersion);
|
||||||
try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {}
|
try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,9 +100,8 @@ module.exports = {
|
||||||
total++;
|
total++;
|
||||||
if (mine[k] >= 0) shared++;
|
if (mine[k] >= 0) shared++;
|
||||||
}
|
}
|
||||||
const v = rank.versions.get(key);
|
const carriesMe = follows[server.id] >= 0 ? 1 : 0;
|
||||||
const tier = v ? (upToDate(v) ? 2 : 0) : (total > 0 ? 0 : 1);
|
return carriesMe * 1e12 + versionTier(rank.versions.get(key), total) * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999);
|
||||||
return tier * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999);
|
|
||||||
};
|
};
|
||||||
server.oasisPeerRank = peerRank;
|
server.oasisPeerRank = peerRank;
|
||||||
setImmediate(refreshRank);
|
setImmediate(refreshRank);
|
||||||
|
|
|
||||||
11
src/server/package-lock.json
generated
11
src/server/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.2.3",
|
"version": "1.3.4",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"mupdf": "^1.28.1",
|
||||||
"open": "^8.4.2",
|
"open": "^8.4.2",
|
||||||
"openpgp": "^6.3.0",
|
"openpgp": "^6.3.0",
|
||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
|
|
@ -4961,6 +4962,12 @@
|
||||||
"non-private-ip": "^2.0.0"
|
"non-private-ip": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mupdf": {
|
||||||
|
"version": "1.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mupdf/-/mupdf-1.28.1.tgz",
|
||||||
|
"integrity": "sha512-Gi11Ow2G1SlrXKJNZBL1eAIGFVih5+4ZKqjptamTVaj/5hnlrcVrVbyb7lHE2lfFKdxZPyv9ZtfOOq7XgjzEig==",
|
||||||
|
"license": "AGPL-3.0-or-later"
|
||||||
|
},
|
||||||
"node_modules/mutexify": {
|
"node_modules/mutexify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"description": "Oasis - Social Networking Utopia",
|
"description": "Oasis - Social Networking Utopia",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"start:ssb": "node SSB_server.js start &",
|
"start:ssb": "node SSB_server.js start &",
|
||||||
"dev": "npm run dev:backend",
|
"dev": "npm run dev:backend",
|
||||||
"dev:backend": "npx --yes nodemon@3.1.14",
|
"dev:backend": "npx --yes nodemon@3.1.14",
|
||||||
"postinstall": "node ../../scripts/patch-node-modules.js"
|
"postinstall": "node ../patches/patch-node-modules.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/router": "^13.1.0",
|
"@koa/router": "^13.1.0",
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"mupdf": "^1.28.1",
|
||||||
"open": "^8.4.2",
|
"open": "^8.4.2",
|
||||||
"openpgp": "^6.3.0",
|
"openpgp": "^6.3.0",
|
||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawn } = require('child_process');
|
const { spawn, execFile } = require('child_process');
|
||||||
const nodeCrypto = require('crypto');
|
const nodeCrypto = require('crypto');
|
||||||
const pull = require('./node_modules/pull-stream');
|
const pull = require('./node_modules/pull-stream');
|
||||||
const Pushable = require('./node_modules/pull-pushable');
|
const Pushable = require('./node_modules/pull-pushable');
|
||||||
|
|
@ -64,6 +64,10 @@ const VIDEO_FRAME_MAX = 256 * 1024;
|
||||||
const VIDEO_FPS = 8;
|
const VIDEO_FPS = 8;
|
||||||
const VIDEO_SIZE = { w: 320, h: 240 };
|
const VIDEO_SIZE = { w: 320, h: 240 };
|
||||||
const STREAM_SIZE = { w: 640, h: 480 };
|
const STREAM_SIZE = { w: 640, h: 480 };
|
||||||
|
const SCREEN_SIZE = { w: 1280, h: 720 };
|
||||||
|
const SCREEN_FPS = 2;
|
||||||
|
const SCREEN_QUALITY = 12;
|
||||||
|
const SCREEN_PROBE_MS = 5000;
|
||||||
const GRID_TILE_LARGE = { w: 640, h: 480 };
|
const GRID_TILE_LARGE = { w: 640, h: 480 };
|
||||||
const GRID_LARGE_MAX = 4;
|
const GRID_LARGE_MAX = 4;
|
||||||
const ROOM_VIDEO_CHUNK_EXTRA = 45;
|
const ROOM_VIDEO_CHUNK_EXTRA = 45;
|
||||||
|
|
@ -245,11 +249,7 @@ const STREAM_BACKENDS = [
|
||||||
];
|
];
|
||||||
const STREAM_STALL_MS = 5000;
|
const STREAM_STALL_MS = 5000;
|
||||||
const STREAM_START_MS = 20000;
|
const STREAM_START_MS = 20000;
|
||||||
const systemStream = () => {
|
const frameProcess = ({ bin, args, retry }, { onJpeg, onPcm, onEnd }) => {
|
||||||
const backend = STREAM_BACKENDS.find(b => hasBin(b.bin()));
|
|
||||||
if (!backend) return null;
|
|
||||||
return {
|
|
||||||
open(file, { onJpeg, onPcm, onEnd }) {
|
|
||||||
let proc = null;
|
let proc = null;
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
let lastAt = 0;
|
let lastAt = 0;
|
||||||
|
|
@ -262,29 +262,63 @@ const systemStream = () => {
|
||||||
if (watch.unref) watch.unref();
|
if (watch.unref) watch.unref();
|
||||||
const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} };
|
const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} };
|
||||||
const finish = () => { if (ended) return; halt(); onEnd(); };
|
const finish = () => { if (ended) return; halt(); onEnd(); };
|
||||||
const run = (withAudio) => {
|
const run = (first) => {
|
||||||
const p = spawn(backend.bin(), backend.args(file, withAudio), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] });
|
const p = spawn(bin, args(first), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] });
|
||||||
proc = p;
|
proc = p;
|
||||||
p.on('error', () => {});
|
p.on('error', () => {});
|
||||||
p.stdout.on('error', () => {});
|
p.stdout.on('error', () => {});
|
||||||
p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); }));
|
p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); }));
|
||||||
if (p.stdio[3]) {
|
if (p.stdio[3]) {
|
||||||
p.stdio[3].on('error', () => {});
|
p.stdio[3].on('error', () => {});
|
||||||
p.stdio[3].on('data', (pcm) => { if (!ended && proc === p) onPcm(pcm); });
|
p.stdio[3].on('data', (pcm) => { if (!ended && proc === p && onPcm) onPcm(pcm); });
|
||||||
}
|
}
|
||||||
p.on('close', () => {
|
p.on('close', () => {
|
||||||
if (ended || proc !== p) return;
|
if (ended || proc !== p) return;
|
||||||
if (withAudio && !frames && backend.retryWithoutAudio) return run(false);
|
if (first && !frames && retry) return run(false);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
run(true);
|
run(true);
|
||||||
return { stop() { if (!ended) halt(); } };
|
return { stop() { if (!ended) halt(); } };
|
||||||
|
};
|
||||||
|
const systemStream = () => {
|
||||||
|
const backend = STREAM_BACKENDS.find(b => hasBin(b.bin()));
|
||||||
|
if (!backend) return null;
|
||||||
|
return {
|
||||||
|
open(file, handlers) {
|
||||||
|
return frameProcess({ bin: backend.bin(), args: (withAudio) => backend.args(file, withAudio), retry: !!backend.retryWithoutAudio }, handlers);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let streamFactory = systemStream;
|
let streamFactory = systemStream;
|
||||||
|
|
||||||
|
const x11Screen = (env) => (env.DISPLAY && !env.WAYLAND_DISPLAY && String(env.XDG_SESSION_TYPE || '').toLowerCase() !== 'wayland'
|
||||||
|
? { device: 'x11grab', input: ['-f', 'x11grab', '-framerate', String(SCREEN_FPS), '-i', env.DISPLAY] }
|
||||||
|
: null);
|
||||||
|
const SCREEN_SOURCES = {
|
||||||
|
win32: () => ({ device: 'gdigrab', input: ['-f', 'gdigrab', '-framerate', String(SCREEN_FPS), '-i', 'desktop'] }),
|
||||||
|
darwin: () => ({ device: 'avfoundation', input: ['-f', 'avfoundation', '-capture_cursor', '1', '-i', 'Capture screen 0:none'] }),
|
||||||
|
android: () => null
|
||||||
|
};
|
||||||
|
const screenSource = (platform = process.platform, env = process.env) => (SCREEN_SOURCES[platform] || x11Screen)(env || {});
|
||||||
|
const screenArgs = (source) => [
|
||||||
|
'-loglevel', 'quiet', ...source.input,
|
||||||
|
'-vf', `fps=${SCREEN_FPS},scale=${SCREEN_SIZE.w}:${SCREEN_SIZE.h}:force_original_aspect_ratio=decrease:flags=lanczos,pad=${SCREEN_SIZE.w}:${SCREEN_SIZE.h}:(ow-iw)/2:(oh-ih)/2`,
|
||||||
|
'-c:v', 'mjpeg', '-q:v', String(SCREEN_QUALITY), '-f', 'image2pipe', 'pipe:1'
|
||||||
|
];
|
||||||
|
const systemScreen = () => {
|
||||||
|
const source = screenSource();
|
||||||
|
const bin = process.env.OASIS_FFMPEG || 'ffmpeg';
|
||||||
|
if (!source || !hasBin(bin)) return null;
|
||||||
|
let ready = false;
|
||||||
|
execFile(bin, ['-hide_banner', '-devices'], { timeout: SCREEN_PROBE_MS }, (err, out) => { ready = !err && String(out || '').split(/\s+/).includes(source.device); });
|
||||||
|
return {
|
||||||
|
available: () => ready,
|
||||||
|
open(handlers) { return frameProcess({ bin, args: () => screenArgs(source), retry: false }, { onJpeg: handlers.onJpeg, onEnd: handlers.onEnd }); }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let screenFactory = systemScreen;
|
||||||
|
|
||||||
const videoChunks = (seq, jpeg, track = 0) => {
|
const videoChunks = (seq, jpeg, track = 0) => {
|
||||||
const n = Math.ceil(jpeg.length / VIDEO_CHUNK);
|
const n = Math.ceil(jpeg.length / VIDEO_CHUNK);
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
@ -526,6 +560,7 @@ module.exports = {
|
||||||
const roomQuietAt = Number(policy().roomQuietAt) > 0 ? Number(policy().roomQuietAt) : ROOM_QUIET_AT;
|
const roomQuietAt = Number(policy().roomQuietAt) > 0 ? Number(policy().roomQuietAt) : ROOM_QUIET_AT;
|
||||||
const video = videoFactory(server, policy().videoDevice);
|
const video = videoFactory(server, policy().videoDevice);
|
||||||
const streamer = streamFactory(server);
|
const streamer = streamFactory(server);
|
||||||
|
const screener = screenFactory(server);
|
||||||
const videoAllowed = () => policy().video !== false;
|
const videoAllowed = () => policy().video !== false;
|
||||||
const videoOn = () => !!video && videoAllowed();
|
const videoOn = () => !!video && videoAllowed();
|
||||||
const codecsOffered = () => [...(OpusScript && policy().opus !== false ? ['opus'] : []), ...(videoOn() ? ['mjpeg'] : [])];
|
const codecsOffered = () => [...(OpusScript && policy().opus !== false ? ['opus'] : []), ...(videoOn() ? ['mjpeg'] : [])];
|
||||||
|
|
@ -1120,6 +1155,7 @@ module.exports = {
|
||||||
secure: !!r.kid, count: humansIn(r) + 1, max: r.max, public: [...r.peers.values()].some(p => p.viewer), speaking: now - r.sentAt < 1000,
|
secure: !!r.kid, count: humansIn(r) + 1, max: r.max, public: [...r.peers.values()].some(p => p.viewer), speaking: now - r.sentAt < 1000,
|
||||||
cam: !!r.cam, videoAvailable: videoOn() && r.hubVideo === true, grid: !!sharpLib, gridOrder: gridIds(r),
|
cam: !!r.cam, videoAvailable: videoOn() && r.hubVideo === true, grid: !!sharpLib, gridOrder: gridIds(r),
|
||||||
streaming: r.stream ? r.stream.name : null, streamAvailable: fileStreamOn() && r.hubVideo === true,
|
streaming: r.stream ? r.stream.name : null, streamAvailable: fileStreamOn() && r.hubVideo === true,
|
||||||
|
sharingScreen: !!(r.stream && r.stream.screen), screenAvailable: screenOn() && r.hubVideo === true,
|
||||||
notify: !!r.notify, recording: !!r.rec, recordingSince: r.rec ? r.rec.at : 0, recordingBy: [...r.recPeers],
|
notify: !!r.notify, recording: !!r.rec, recordingSince: r.rec ? r.rec.at : 0, recordingBy: [...r.recPeers],
|
||||||
events: r.notify ? r.events.slice(-ROOM_EVENTS_MAX) : [],
|
events: r.notify ? r.events.slice(-ROOM_EVENTS_MAX) : [],
|
||||||
peers: [...r.peers.values()].map(p => ({ id: p.id, speaking: now - p.heardAt < 1000, muted: now - p.mutedAt < MUTE_TTL_MS, hand: now - (p.handBeat || 0) < MUTE_TTL_MS, handSince: p.handSince || 0, silenced: silenced.has(p.id), recording: r.recPeers.has(p.id), cam: now - (p.frameAt || 0) < VIDEO_HOLD_MS, film: now - (p.filmAt || 0) < VIDEO_HOLD_MS, viewer: !!p.viewer }))
|
peers: [...r.peers.values()].map(p => ({ id: p.id, speaking: now - p.heardAt < 1000, muted: now - p.mutedAt < MUTE_TTL_MS, hand: now - (p.handBeat || 0) < MUTE_TTL_MS, handSince: p.handSince || 0, silenced: silenced.has(p.id), recording: r.recPeers.has(p.id), cam: now - (p.frameAt || 0) < VIDEO_HOLD_MS, film: now - (p.filmAt || 0) < VIDEO_HOLD_MS, viewer: !!p.viewer }))
|
||||||
|
|
@ -1396,6 +1432,7 @@ module.exports = {
|
||||||
r.out.push(Buffer.concat([Buffer.from([K_VIDEO]), Buffer.from(r.kid, 'hex'), r.sealer(plain)]));
|
r.out.push(Buffer.concat([Buffer.from([K_VIDEO]), Buffer.from(r.kid, 'hex'), r.sealer(plain)]));
|
||||||
};
|
};
|
||||||
const fileStreamOn = () => videoAllowed() && !!streamer;
|
const fileStreamOn = () => videoAllowed() && !!streamer;
|
||||||
|
const screenOn = () => videoAllowed() && !!screener && (typeof screener.available !== 'function' || !!screener.available());
|
||||||
const streamFile = (file) => {
|
const streamFile = (file) => {
|
||||||
const raw = String(file || '');
|
const raw = String(file || '');
|
||||||
if (!path.isAbsolute(raw)) return null;
|
if (!path.isAbsolute(raw)) return null;
|
||||||
|
|
@ -1427,13 +1464,12 @@ module.exports = {
|
||||||
if (notify) roomChanged();
|
if (notify) roomChanged();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const startRoomStream = (r, file, name) => {
|
const startFilm = (r, name, screen, open) => {
|
||||||
if (!r || r.phase !== 'live' || !r.hubVideo || !fileStreamOn()) return false;
|
|
||||||
stopRoomStream(r, false);
|
stopRoomStream(r, false);
|
||||||
const s = { name: String(name || '').slice(0, 120), pcm: Buffer.alloc(0), seq: 0, handle: null };
|
const s = { name: String(name || '').slice(0, 120), screen, pcm: Buffer.alloc(0), seq: 0, handle: null };
|
||||||
r.stream = s;
|
r.stream = s;
|
||||||
try {
|
try {
|
||||||
s.handle = streamer.open(file, {
|
s.handle = open({
|
||||||
onJpeg: (jpeg) => {
|
onJpeg: (jpeg) => {
|
||||||
if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return;
|
if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return;
|
||||||
s.seq = (s.seq + 1) & 0xffff;
|
s.seq = (s.seq + 1) & 0xffff;
|
||||||
|
|
@ -1442,7 +1478,7 @@ module.exports = {
|
||||||
sendRoomJpeg(r, s.seq, jpeg, 1);
|
sendRoomJpeg(r, s.seq, jpeg, 1);
|
||||||
},
|
},
|
||||||
onPcm: (pcm) => {
|
onPcm: (pcm) => {
|
||||||
if (r.stream !== s || !pcm || !pcm.length) return;
|
if (r.stream !== s || screen || !pcm || !pcm.length) return;
|
||||||
s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm);
|
s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm);
|
||||||
const drop = (s.pcm.length - RATE * 2) & ~1;
|
const drop = (s.pcm.length - RATE * 2) & ~1;
|
||||||
if (drop > 0) s.pcm = s.pcm.subarray(drop);
|
if (drop > 0) s.pcm = s.pcm.subarray(drop);
|
||||||
|
|
@ -1453,6 +1489,9 @@ module.exports = {
|
||||||
if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; }
|
if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; }
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
const filmReady = (r) => !!r && r.phase === 'live' && !!r.hubVideo;
|
||||||
|
const startRoomStream = (r, file, name) => filmReady(r) && fileStreamOn() && startFilm(r, name, false, (h) => streamer.open(file, h));
|
||||||
|
const startRoomScreen = (r) => filmReady(r) && screenOn() && startFilm(r, '', true, (h) => screener.open(h));
|
||||||
const streamFrame = (r) => {
|
const streamFrame = (r) => {
|
||||||
const s = r.stream;
|
const s = r.stream;
|
||||||
if (!s || s.pcm.length < FRAME_BYTES) return null;
|
if (!s || s.pcm.length < FRAME_BYTES) return null;
|
||||||
|
|
@ -1798,6 +1837,11 @@ module.exports = {
|
||||||
const r = room;
|
const r = room;
|
||||||
if (!r || r.phase !== 'live') return cb(new Error('no-room'));
|
if (!r || r.phase !== 'live') return cb(new Error('no-room'));
|
||||||
if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); }
|
if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); }
|
||||||
|
if (o.screen) {
|
||||||
|
if (!startRoomScreen(r)) return cb(new Error('unavailable'));
|
||||||
|
roomChanged();
|
||||||
|
return cb(null, roomSnapshot());
|
||||||
|
}
|
||||||
const file = streamFile(o.file);
|
const file = streamFile(o.file);
|
||||||
if (!file) return cb(new Error('invalid'));
|
if (!file) return cb(new Error('invalid'));
|
||||||
if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable'));
|
if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable'));
|
||||||
|
|
@ -2108,6 +2152,8 @@ module.exports = {
|
||||||
module.exports.setAudioFactory = (factory) => { audioFactory = typeof factory === 'function' ? factory : systemAudio; };
|
module.exports.setAudioFactory = (factory) => { audioFactory = typeof factory === 'function' ? factory : systemAudio; };
|
||||||
module.exports.setVideoFactory = (factory) => { videoFactory = typeof factory === 'function' ? factory : systemVideo; };
|
module.exports.setVideoFactory = (factory) => { videoFactory = typeof factory === 'function' ? factory : systemVideo; };
|
||||||
module.exports.setStreamFactory = (factory) => { streamFactory = typeof factory === 'function' ? factory : systemStream; };
|
module.exports.setStreamFactory = (factory) => { streamFactory = typeof factory === 'function' ? factory : systemStream; };
|
||||||
|
module.exports.setScreenFactory = (factory) => { screenFactory = typeof factory === 'function' ? factory : systemScreen; };
|
||||||
|
module.exports.screenSource = screenSource;
|
||||||
module.exports.jpegSplitter = jpegSplitter;
|
module.exports.jpegSplitter = jpegSplitter;
|
||||||
module.exports.VIDEO_CHUNK = VIDEO_CHUNK;
|
module.exports.VIDEO_CHUNK = VIDEO_CHUNK;
|
||||||
module.exports.VIDEO_HOLD_MS = VIDEO_HOLD_MS;
|
module.exports.VIDEO_HOLD_MS = VIDEO_HOLD_MS;
|
||||||
|
|
|
||||||
36
src/server/phone_screen_android.js
Normal file
36
src/server/phone_screen_android.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const net = require('net');
|
||||||
|
const path = require('path');
|
||||||
|
const { jpegSplitter } = require('./phone_module');
|
||||||
|
|
||||||
|
const MARKER = path.join(__dirname, '..', '..', '.audio_bridge');
|
||||||
|
|
||||||
|
const bridgePort = () => {
|
||||||
|
try {
|
||||||
|
const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim());
|
||||||
|
return Number.isFinite(port) && port > 0 ? port : 0;
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => ({
|
||||||
|
available: () => bridgePort() > 0,
|
||||||
|
open({ onJpeg, onEnd }) {
|
||||||
|
const port = bridgePort();
|
||||||
|
if (!port) return null;
|
||||||
|
let ended = false;
|
||||||
|
const finish = () => {
|
||||||
|
if (ended) return;
|
||||||
|
ended = true;
|
||||||
|
try { onEnd(); } catch (_) {}
|
||||||
|
};
|
||||||
|
const sock = net.connect(port, '127.0.0.1');
|
||||||
|
sock.setNoDelay(true);
|
||||||
|
sock.on('error', finish);
|
||||||
|
sock.on('close', finish);
|
||||||
|
sock.on('connect', () => sock.write('C'));
|
||||||
|
sock.on('data', jpegSplitter((jpeg) => { if (!ended) onJpeg(jpeg); }));
|
||||||
|
return { stop() { ended = true; try { sock.destroy(); } catch (_) {} } };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -46,6 +46,14 @@ config.blobs.max = 75 * megabyte;
|
||||||
|
|
||||||
config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) };
|
config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) };
|
||||||
|
|
||||||
|
const envPort = Number(process.env.ssb_port);
|
||||||
|
if (Number.isInteger(envPort) && envPort > 0 && envPort < 65536) {
|
||||||
|
config.port = envPort;
|
||||||
|
for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) {
|
||||||
|
if (entry && typeof entry === 'object') entry.port = envPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })();
|
const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })();
|
||||||
const bindHost = config.pub ? '0.0.0.0' : lanOn ? null : '127.0.0.1';
|
const bindHost = config.pub ? '0.0.0.0' : lanOn ? null : '127.0.0.1';
|
||||||
for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) {
|
for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ async function checkForUpdate() {
|
||||||
await updater.getRemoteVersion();
|
await updater.getRemoteVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', offline = false, isPublic = false) {
|
async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', isPublic = false) {
|
||||||
if (printed) return;
|
if (printed) return;
|
||||||
printed = true;
|
printed = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
|
const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
|
||||||
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||||
const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction } = require('./main_views');
|
const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderCardTop, renderShareButton, renderOpinionsPill, renderEngagement, commentCountFor, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction, renderPdfDocument, forumReplyAnchor, renderCommentsSummary, commentsOpenHref } = require('./main_views');
|
||||||
const opinionCategories = require('../backend/opinion_categories');
|
const opinionCategories = require('../backend/opinion_categories');
|
||||||
const { roomNumberOf } = require('../models/phone_number');
|
const { roomNumberOf } = require('../models/phone_number');
|
||||||
const mobileUi = require("./mobile_ui");
|
const mobileUi = require("./mobile_ui");
|
||||||
|
|
@ -200,7 +200,9 @@ function buildActivityItemsWithPostThreads(deduped, allActions) {
|
||||||
.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
||||||
|
|
||||||
if (!rootAction) continue;
|
if (!rootAction) continue;
|
||||||
const rootType = rootAction.type || 'post';
|
const rootUnknown = !byId.has(threadId) && getThreadIdFromPost(rootAction) !== safeMsgId(rootAction);
|
||||||
|
const located = rootUnknown ? commentOf(rootAction) : null;
|
||||||
|
const rootType = rootUnknown ? ((located && located.rootType) || 'comment') : (rootAction.type || 'post');
|
||||||
out.push({
|
out.push({
|
||||||
id: `thread:${threadId}`,
|
id: `thread:${threadId}`,
|
||||||
type: 'postThread',
|
type: 'postThread',
|
||||||
|
|
@ -209,7 +211,7 @@ function buildActivityItemsWithPostThreads(deduped, allActions) {
|
||||||
content: {
|
content: {
|
||||||
threadId,
|
threadId,
|
||||||
rootType,
|
rootType,
|
||||||
rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : getViewDetailsAction(rootType, rootAction),
|
rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : rootUnknown ? commentHref({ rootId: threadId, rootType: located && located.rootType }) : getViewDetailsAction(rootType, rootAction),
|
||||||
root: rootAction
|
root: rootAction
|
||||||
? {
|
? {
|
||||||
id: safeMsgId(rootAction),
|
id: safeMsgId(rootAction),
|
||||||
|
|
@ -256,12 +258,15 @@ const renderTribeOpenedCard = (action, date, authorName) => {
|
||||||
: null;
|
: null;
|
||||||
const bookmarkHref = c.mediaType === 'bookmark' ? safeExternalHref(c.url) : '';
|
const bookmarkHref = c.mediaType === 'bookmark' ? safeExternalHref(c.url) : '';
|
||||||
return div({ class: 'trending-card' },
|
return div({ class: 'trending-card' },
|
||||||
div({ class: 'card-header activity-card-header' },
|
renderCardTop(null, origin.href, {
|
||||||
div({ class: 'card-chips-row' },
|
author: action.author,
|
||||||
|
authorName,
|
||||||
|
date: action.ts,
|
||||||
|
chips: [div({ class: 'card-chips-row' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, label)),
|
span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, label)),
|
||||||
renderTribeOriginChip(origin)
|
renderTribeOriginChip(origin)
|
||||||
)
|
)]
|
||||||
),
|
}),
|
||||||
div({ class: 'card-section' },
|
div({ class: 'card-section' },
|
||||||
c.title ? h2(origin.href ? a({ href: origin.href }, c.title) : c.title) : null,
|
c.title ? h2(origin.href ? a({ href: origin.href }, c.title) : c.title) : null,
|
||||||
c.description ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(c.description).slice(0, 600))) : null,
|
c.description ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(c.description).slice(0, 600))) : null,
|
||||||
|
|
@ -269,10 +274,7 @@ const renderTribeOpenedCard = (action, date, authorName) => {
|
||||||
media,
|
media,
|
||||||
bookmarkHref ? a({ href: bookmarkHref, target: '_blank', rel: 'noopener noreferrer' }, c.url) : null
|
bookmarkHref ? a({ href: bookmarkHref, target: '_blank', rel: 'noopener noreferrer' }, c.url) : null
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
origin.href ? renderCardMetaRow(renderShareButton(safeMsgId(action.id || action.key), origin.href, c.title || '')) : null
|
||||||
span({ class: 'date-link' }, `${date}`),
|
|
||||||
userLink(action.author, authorName)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -404,6 +406,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
headerText = `[${String(i18n.typePodcast || 'PODCAST').toUpperCase()} · ${String(i18n.typePodcastEpisode || 'EPISODE').toUpperCase()}]`;
|
headerText = `[${String(i18n.typePodcast || 'PODCAST').toUpperCase()} · ${String(i18n.typePodcastEpisode || 'EPISODE').toUpperCase()}]`;
|
||||||
} else if (type === 'larpHousePost') {
|
} else if (type === 'larpHousePost') {
|
||||||
headerText = `[${String(i18n.typeLarp || 'L.A.R.P.').toUpperCase()}]`;
|
headerText = `[${String(i18n.typeLarp || 'L.A.R.P.').toUpperCase()}]`;
|
||||||
|
} else if (type === 'post' && commentOf(action)) {
|
||||||
|
headerText = `[${String(commentLabel(commentOf(action))).toUpperCase()}]`;
|
||||||
} else {
|
} else {
|
||||||
const typeLabel = i18n[`type${capitalize(type)}`] || type;
|
const typeLabel = i18n[`type${capitalize(type)}`] || type;
|
||||||
headerText = `[${String(typeLabel).toUpperCase()}]`;
|
headerText = `[${String(typeLabel).toUpperCase()}]`;
|
||||||
|
|
@ -417,7 +421,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
const commentCount =
|
const commentCount =
|
||||||
typeof action.commentCount === 'number'
|
typeof action.commentCount === 'number'
|
||||||
? action.commentCount
|
? action.commentCount
|
||||||
: (typeof content.commentCount === 'number' ? content.commentCount : 0);
|
: (typeof content.commentCount === 'number' ? content.commentCount : commentCountFor([action.id, action.rootId, action.key]));
|
||||||
|
|
||||||
const votesList = votes && typeof votes === 'object'
|
const votesList = votes && typeof votes === 'object'
|
||||||
? Object.entries(votes).map(([option, count]) => ({ option, count }))
|
? Object.entries(votes).map(([option, count]) => ({ option, count }))
|
||||||
|
|
@ -517,7 +521,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }),
|
img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }),
|
||||||
span(`L.A.R.P · ${String(house).toUpperCase()}`)
|
span(`L.A.R.P · ${String(house).toUpperCase()}`)
|
||||||
) : null,
|
) : null,
|
||||||
text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text))) : null
|
text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text), { zoomImages: true })) : null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -699,11 +703,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
cardBody.push(
|
cardBody.push(
|
||||||
div({ class: 'card-section document' },
|
div({ class: 'card-section document' },
|
||||||
title?.trim() ? h2({ class: 'document-title' }, title) : "",
|
title?.trim() ? h2({ class: 'document-title' }, title) : "",
|
||||||
div({
|
renderPdfDocument(`/blob/${encodeURIComponent(url)}`, { title })
|
||||||
id: `pdf-container-${key || url}`,
|
|
||||||
class: 'pdf-viewer-container',
|
|
||||||
'data-pdf-url': `/blob/${encodeURIComponent(url)}`
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -813,13 +813,14 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
bodyNode = div({ class: 'feed-text post-text post-text-clamped', [RAW_HTML]: sanitizeHtml(linkified) });
|
bodyNode = div({ class: 'feed-text post-text post-text-clamped', [RAW_HTML]: sanitizeHtml(linkified) });
|
||||||
} else {
|
} else {
|
||||||
bodyNode = div({ class: 'feed-text' },
|
bodyNode = div({ class: 'feed-text' },
|
||||||
p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText))
|
p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText, { zoomImages: true }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const threadId = getThreadIdFromPost(action);
|
const threadId = getThreadIdFromPost(action);
|
||||||
const replyToId = getReplyToIdFromPost(action, byIdAll);
|
const replyToId = getReplyToIdFromPost(action, byIdAll);
|
||||||
const isReply = !!(threadId && threadId !== action.id);
|
const isReply = !!(threadId && threadId !== action.id);
|
||||||
const ctxHref = isReply ? `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(replyToId || threadId)}` : '';
|
const onContent = commentOf(action) && commentOf(action).rootType && commentOf(action).rootType !== 'post' ? commentOf(action) : null;
|
||||||
|
const ctxHref = !isReply ? '' : onContent ? commentHref({ rootId: threadId, rootType: onContent.rootType }) : `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(replyToId || threadId)}`;
|
||||||
const parent = isReply ? (byIdAll.get(replyToId) || byIdAll.get(threadId)) : null;
|
const parent = isReply ? (byIdAll.get(replyToId) || byIdAll.get(threadId)) : null;
|
||||||
const parentContent = parent ? (parent.value?.content || parent.content || {}) : {};
|
const parentContent = parent ? (parent.value?.content || parent.content || {}) : {};
|
||||||
const parentAuthor = parent?.author || '';
|
const parentAuthor = parent?.author || '';
|
||||||
|
|
@ -853,10 +854,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
const rootType = c.rootType || 'post';
|
const rootType = c.rootType || 'post';
|
||||||
const isBlogThread = rootType === 'post';
|
const isBlogThread = rootType === 'post';
|
||||||
const baseHref = c.rootHref || `/blogs/${encodeURIComponent(threadId)}`;
|
const baseHref = c.rootHref || `/blogs/${encodeURIComponent(threadId)}`;
|
||||||
const href = isBlogThread ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`;
|
const href = isBlogThread || baseHref.includes('comments=open') ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`;
|
||||||
const rootTypeLabel = isBlogThread
|
const threadLabel = rootType === 'comment' ? i18n.typeComment : `${rootTypeLabel(rootType)} · ${i18n.typeComment}`;
|
||||||
? (i18n.typePost || 'BLOG')
|
|
||||||
: (i18n[`type${rootType.charAt(0).toUpperCase()}${rootType.slice(1)}`] || rootType);
|
|
||||||
const root = c.root;
|
const root = c.root;
|
||||||
const replies = Array.isArray(c.replies) ? c.replies : [];
|
const replies = Array.isArray(c.replies) ? c.replies : [];
|
||||||
const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
||||||
|
|
@ -866,19 +865,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
? (titleSource.length > 90 ? `${titleSource.slice(0, 90)}…` : titleSource)
|
? (titleSource.length > 90 ? `${titleSource.slice(0, 90)}…` : titleSource)
|
||||||
: threadId;
|
: threadId;
|
||||||
return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
|
return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
|
||||||
div({ class: 'card-header activity-card-header' },
|
renderCardTop(threadId, baseHref, {
|
||||||
div({ class: 'card-chips-row' },
|
bylineAuthor: action.author,
|
||||||
|
authorName: action.authorNames && action.authorNames[action.author],
|
||||||
|
date: action.ts,
|
||||||
|
chips: [div({ class: 'card-chips-row' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, `${String(rootTypeLabel).toUpperCase()} · ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`)
|
span({ class: 'pm-exposition-text' }, String(threadLabel).toUpperCase())
|
||||||
)
|
)
|
||||||
),
|
)],
|
||||||
renderContentActions(threadId, baseHref, {
|
|
||||||
author: c.rootAuthor || (root && root.author) || action.author,
|
author: c.rootAuthor || (root && root.author) || action.author,
|
||||||
reportTitle: titleText,
|
reportTitle: titleText,
|
||||||
spread: spreadMap.get(threadId) || null,
|
spread: spreadMap.get(threadId) || null,
|
||||||
...favOptsFor(rootType, threadId, extras)
|
...favOptsFor(rootType, threadId, extras)
|
||||||
})
|
}),
|
||||||
),
|
|
||||||
div({ class: 'card-body' },
|
div({ class: 'card-body' },
|
||||||
div({ class: 'card-section' },
|
div({ class: 'card-section' },
|
||||||
div({ class: 'card-field activity-update-title' },
|
div({ class: 'card-field activity-update-title' },
|
||||||
|
|
@ -887,15 +887,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
),
|
),
|
||||||
latest && latest.text
|
latest && latest.text
|
||||||
? div({ class: 'feed-text activity-update-msg' },
|
? div({ class: 'feed-text activity-update-msg' },
|
||||||
p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text))
|
p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text, { zoomImages: true }))
|
||||||
)
|
)
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
renderCardMetaRow(renderShareButton(threadId, baseHref, titleText))
|
||||||
span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`),
|
|
||||||
userLink(action.author, action.authorNames && action.authorNames[action.author])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -910,19 +907,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
const limit = 2;
|
const limit = 2;
|
||||||
const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit));
|
const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit));
|
||||||
return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
|
return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
|
||||||
div({ class: 'card-header activity-card-header' },
|
renderCardTop(chatRoot, href, {
|
||||||
div({ class: 'card-chips-row' },
|
bylineAuthor: action.author,
|
||||||
|
authorName: action.authorNames && action.authorNames[action.author],
|
||||||
|
date: action.ts,
|
||||||
|
chips: [div({ class: 'card-chips-row' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`)
|
span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`)
|
||||||
)
|
)
|
||||||
),
|
)],
|
||||||
renderContentActions(chatRoot, href, {
|
|
||||||
author: c.chatAuthor || action.author,
|
author: c.chatAuthor || action.author,
|
||||||
reportTitle: chatTitle,
|
reportTitle: chatTitle,
|
||||||
spread: spreadMap.get(chatRoot) || null,
|
spread: spreadMap.get(chatRoot) || null,
|
||||||
...favOptsFor('chat', chatRoot, extras)
|
...favOptsFor('chat', chatRoot, extras)
|
||||||
})
|
}),
|
||||||
),
|
|
||||||
div({ class: 'card-body' },
|
div({ class: 'card-body' },
|
||||||
div({ class: 'card-section chat' },
|
div({ class: 'card-section chat' },
|
||||||
div({ class: 'card-field activity-update-title' },
|
div({ class: 'card-field activity-update-title' },
|
||||||
|
|
@ -947,13 +945,13 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
: renderZoomableImage(attSrc, { imgClass: 'post-image' }))
|
: renderZoomableImage(attSrc, { imgClass: 'post-image' }))
|
||||||
: renderMediaBlob(latest.image);
|
: renderMediaBlob(latest.image);
|
||||||
const textNode = latest.text
|
const textNode = latest.text
|
||||||
? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text))
|
? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text, { zoomImages: true }))
|
||||||
: null;
|
: null;
|
||||||
const quoted = latest.reply || null;
|
const quoted = latest.reply || null;
|
||||||
const replyNode = quoted
|
const replyNode = quoted
|
||||||
? div({ class: 'reply-context' },
|
? div({ class: 'reply-context' },
|
||||||
span({ class: 'reply-context-meta' },
|
span({ class: 'reply-context-meta' },
|
||||||
a({ href: `${href}#msg-${String(quoted.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
|
a({ href: `${href}#${forumReplyAnchor(quoted.id)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
|
||||||
quoted.author ? span(' ', userLink(quoted.author, action.authorNames && action.authorNames[quoted.author])) : ''
|
quoted.author ? span(' ', userLink(quoted.author, action.authorNames && action.authorNames[quoted.author])) : ''
|
||||||
),
|
),
|
||||||
quoted.text ? p({ class: 'post-text reply-context-text' }, quoted.text) : ''
|
quoted.text ? p({ class: 'post-text reply-context-text' }, quoted.text) : ''
|
||||||
|
|
@ -965,10 +963,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
renderCardMetaRow(renderShareButton(chatRoot, href, chatTitle))
|
||||||
span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`),
|
|
||||||
userLink(action.author, action.authorNames && action.authorNames[action.author])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,13 +996,13 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
div(
|
div(
|
||||||
{ class: 'reply-context' },
|
{ class: 'reply-context' },
|
||||||
span({ class: 'reply-context-meta' },
|
span({ class: 'reply-context-meta' },
|
||||||
a({ href: `/forum/${encodeURIComponent(hrefKey)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread),
|
a({ href: `/forum/${encodeURIComponent(hrefKey)}#${forumReplyAnchor(action.tipId || action.id)}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread),
|
||||||
repliedAuthor ? span(' ', userLink(repliedAuthor, repliedName)) : ''
|
repliedAuthor ? span(' ', userLink(repliedAuthor, repliedName)) : ''
|
||||||
),
|
),
|
||||||
parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : ''
|
parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : ''
|
||||||
),
|
),
|
||||||
div({ class: 'card-field forum-reply-body' },
|
div({ class: 'card-field forum-reply-body' },
|
||||||
p({ class: 'forum-reply-text' }, ...renderStyledText(text))
|
p({ class: 'forum-reply-text' }, ...renderStyledText(text, { zoomImages: true }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -1050,7 +1045,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '',
|
spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '',
|
||||||
spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '',
|
spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '',
|
||||||
spreadExcerpt
|
spreadExcerpt
|
||||||
? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt))
|
? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt, { zoomImages: true }))
|
||||||
: div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'),
|
: div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'),
|
||||||
spreadOriginalAuthor
|
spreadOriginalAuthor
|
||||||
? div({ class: 'card-field' },
|
? div({ class: 'card-field' },
|
||||||
|
|
@ -1315,7 +1310,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
cardBody.push(
|
cardBody.push(
|
||||||
div({ class: 'card-section' },
|
div({ class: 'card-section' },
|
||||||
div({ class: 'card-field' }, emergencyKey ? a({ href: `/emergencies/${encodeURIComponent(emergencyKey)}`, class: 'card-value user-link' }, emergencyTitle || emergencyKey) : span({ class: 'card-value' }, emergencyTitle || '')),
|
div({ class: 'card-field' }, emergencyKey ? a({ href: `/emergencies/${encodeURIComponent(emergencyKey)}`, class: 'card-value user-link' }, emergencyTitle || emergencyKey) : span({ class: 'card-value' }, emergencyTitle || '')),
|
||||||
content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '',
|
|
||||||
content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.emergencyCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '',
|
content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.emergencyCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '',
|
||||||
renderMediaBlob(content.text, null),
|
renderMediaBlob(content.text, null),
|
||||||
stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : ''
|
stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : ''
|
||||||
|
|
@ -1526,19 +1520,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
.replace('%ACTION%', () => `<strong>${escapeHtml(actionWord)}</strong>`);
|
.replace('%ACTION%', () => `<strong>${escapeHtml(actionWord)}</strong>`);
|
||||||
|
|
||||||
return div({ class: 'card card-rpg' },
|
return div({ class: 'card card-rpg' },
|
||||||
div({ class: 'card-header' },
|
renderCardTop(null, `/projects/${encodeURIComponent(action.tipId || action.id)}`, {
|
||||||
h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
|
author: action.author,
|
||||||
form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
|
authorName: action.authorNames && action.authorNames[action.author],
|
||||||
button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
|
date: action.ts,
|
||||||
|
chips: [div({ class: 'card-chips-row' },
|
||||||
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
|
span({ class: 'pm-exposition-text' }, (i18n.typeProject || 'PROJECT').toUpperCase())
|
||||||
)
|
)
|
||||||
),
|
)]
|
||||||
|
}),
|
||||||
div(
|
div(
|
||||||
p({ [RAW_HTML]: sanitizeHtml(msgHtml) })
|
p({ [RAW_HTML]: sanitizeHtml(msgHtml) })
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
renderEngagement(safeMsgId(action.tipId || action.id), null, null, { href: `/projects/${encodeURIComponent(action.tipId || action.id)}`, title: title || '' })
|
||||||
span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`),
|
|
||||||
userLink(action.author, action.authorNames && action.authorNames[action.author])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1879,15 +1874,17 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
: null;
|
: null;
|
||||||
const msgId = safeMsgId(action.tipId || action.id || action.key);
|
const msgId = safeMsgId(action.tipId || action.id || action.key);
|
||||||
const isOwn = action.author && String(action.author) === String(userId);
|
const isOwn = action.author && String(action.author) === String(userId);
|
||||||
|
const bylineAuthorId = action.author || (content && content.proposer) || '';
|
||||||
return div({ class: 'trending-card' + (isOwn ? ' own-content' : '') },
|
return div({ class: 'trending-card' + (isOwn ? ' own-content' : '') },
|
||||||
div({ class: 'card-header activity-card-header' },
|
renderCardTop(msgId, detailHref, {
|
||||||
div({ class: 'card-chips-row' },
|
date: action.ts,
|
||||||
|
authorName: (action.authorNames && action.authorNames[bylineAuthorId]) || getProfile(bylineAuthorId).name,
|
||||||
|
chips: [div({ class: 'card-chips-row' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, String(headerText || type || '').replace(/^\[|\]$/g, ''))
|
span({ class: 'pm-exposition-text' }, String(headerText || type || '').replace(/^\[|\]$/g, ''))
|
||||||
)
|
)
|
||||||
),
|
)],
|
||||||
renderContentActions(msgId, detailHref, {
|
author: bylineAuthorId,
|
||||||
author: action.author,
|
|
||||||
contact: type === 'about',
|
contact: type === 'about',
|
||||||
reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '',
|
reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '',
|
||||||
spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined,
|
spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined,
|
||||||
|
|
@ -1895,37 +1892,21 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&')
|
...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&')
|
||||||
? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } }
|
? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } }
|
||||||
: {}),
|
: {}),
|
||||||
...favOptsFor(type, msgId, extras),
|
...(commentOf(action) ? {} : favOptsFor(type, msgId, extras)),
|
||||||
deleteAction: isOwn ? activityDeleteAction(type, action, content, msgId, userId) : undefined
|
deleteAction: isOwn ? (commentOf(action) ? `/comments/delete/${encodeURIComponent(msgId)}` : activityDeleteAction(type, action, content, msgId, userId)) : undefined
|
||||||
})
|
}),
|
||||||
),
|
|
||||||
...cardBody,
|
...cardBody,
|
||||||
(() => {
|
(() => {
|
||||||
const footerAuthorId = action.author || (content && content.proposer) || '';
|
const shareNode = detailHref ? renderShareButton(msgId, detailHref, (content && (content.title || content.question || content.concept || content.name)) || '') : null;
|
||||||
const footerNode = p({ class: 'card-footer' },
|
const commentTotal = typeof action.commentCount === 'number' ? action.commentCount : commentCountFor([action.id, action.rootId, action.key, msgId]);
|
||||||
span({ class: 'date-link' }, `${date}`),
|
const commentsHref = !detailHref ? '' : commentOf(action) ? detailHref : commentsOpenHref(detailHref, commentTotal);
|
||||||
userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name)
|
const commentsNode = type !== 'about' ? renderCommentsSummary(commentsHref, commentTotal) : null;
|
||||||
);
|
|
||||||
const commentTotal = Number(action.commentCount) || 0;
|
|
||||||
const commentsHref = detailHref
|
|
||||||
? `${detailHref}${detailHref.includes('?') ? '&' : '?'}comments=open${commentTotal ? '#comments-latest' : ''}`
|
|
||||||
: '';
|
|
||||||
const commentsNode = detailHref && type !== 'about'
|
|
||||||
? a({
|
|
||||||
href: commentsHref,
|
|
||||||
class: commentTotal ? 'comments-summary-link engage-on' : 'comments-summary-link',
|
|
||||||
title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments'
|
|
||||||
},
|
|
||||||
span({ class: 'comments-summary-icon' }, '✑'),
|
|
||||||
span({ class: 'comments-summary-count' }, `(${commentTotal})`)
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const routeFn = OPINION_TYPES.has(type) ? OPINION_ROUTES[type] : null;
|
const routeFn = OPINION_TYPES.has(type) ? OPINION_ROUTES[type] : null;
|
||||||
if (!routeFn) return commentsNode ? [renderCardMetaRow(commentsNode, footerNode)] : footerNode;
|
if (!routeFn) return renderCardMetaRow(type !== 'about' && detailHref ? renderOpinionsPill(msgId, detailHref) : null, commentsNode, shareNode);
|
||||||
const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {};
|
const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {};
|
||||||
const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0);
|
const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||||
const votingNode = details({ class: 'opinions-voting-collapse' },
|
const votingNode = details({ class: 'opinions-voting-collapse' },
|
||||||
summary({ class: 'opinions-summary' },
|
summary({ class: opsTotal > 0 ? 'opinions-summary engage-on' : 'opinions-summary' },
|
||||||
span({ class: 'opinions-summary-icon' }, 'ꔍ'),
|
span({ class: 'opinions-summary-icon' }, 'ꔍ'),
|
||||||
span({ class: 'opinions-summary-count' }, `(${opsTotal})`)),
|
span({ class: 'opinions-summary-count' }, `(${opsTotal})`)),
|
||||||
div({ class: 'voting-buttons' },
|
div({ class: 'voting-buttons' },
|
||||||
|
|
@ -1936,7 +1917,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, footerNode)];
|
return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, shareNode)];
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -1974,6 +1955,51 @@ const favOptsFor = (type, id, extras = {}) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commentOf = (action) => action && action.comment && action.comment.rootId ? action.comment : null;
|
||||||
|
|
||||||
|
function commentHref(comment, commentId) {
|
||||||
|
const rootId = comment && comment.rootId;
|
||||||
|
if (!rootId) return commentId ? `/thread/${encodeURIComponent(commentId)}` : '/activity';
|
||||||
|
if (comment.rootType === 'forum') return `/forum/${encodeURIComponent(rootId)}${commentId ? `#${forumReplyAnchor(commentId)}` : ''}`;
|
||||||
|
const tail = commentId ? `#${encodeURIComponent(commentId)}` : '';
|
||||||
|
const base = comment.rootType === 'post' ? `/blogs/${encodeURIComponent(rootId)}` : comment.rootType ? getViewDetailsAction(comment.rootType, { id: rootId }) : null;
|
||||||
|
if (!base || base === '/activity') return `/thread/${encodeURIComponent(commentId || rootId)}`;
|
||||||
|
if (base.includes('#')) return base;
|
||||||
|
return `${base}${base.includes('?') ? '&' : '?'}comments=open${tail || '#comments-latest'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUP_SUBTYPES = {
|
||||||
|
parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'],
|
||||||
|
courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'],
|
||||||
|
task: ['task', 'taskAssignment'],
|
||||||
|
votes: ['votes', 'poll'],
|
||||||
|
shop: ['shop', 'shopProduct'],
|
||||||
|
larp: ['larpHousePost'],
|
||||||
|
inhabitants:['about'],
|
||||||
|
chat: ['chat', 'chatThread'],
|
||||||
|
wiki: ['wikiPage'],
|
||||||
|
mailing: ['mailingList'],
|
||||||
|
emergency: ['emergency', 'emergencyUpdate'],
|
||||||
|
logistics: ['logisticsRoute'],
|
||||||
|
podcast: ['podcast', 'podcastEpisode'],
|
||||||
|
campaign: ['campaign', 'campaignUpdate'],
|
||||||
|
industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation']
|
||||||
|
};
|
||||||
|
const MODULE_OF_TYPE = Object.entries(GROUP_SUBTYPES).reduce((acc, [mod, types]) => {
|
||||||
|
for (const t of types) if (t !== mod && t !== 'poll') acc[t] = mod;
|
||||||
|
return acc;
|
||||||
|
}, { schoolCourse: 'school', mapMarker: 'map' });
|
||||||
|
|
||||||
|
function rootTypeLabel(rt) {
|
||||||
|
return rt === 'post' ? (i18n.typePost || 'BLOG') : rt === 'poll' ? (i18n.pollsTitle || 'POLLS') : rt ? (i18n[`type${capitalize(MODULE_OF_TYPE[rt] || rt)}`] || rt) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentLabel(comment) {
|
||||||
|
const base = rootTypeLabel(comment && comment.rootType);
|
||||||
|
const word = i18n.typeComment;
|
||||||
|
return base ? `${base} · ${word}` : word;
|
||||||
|
}
|
||||||
|
|
||||||
function getViewDetailsAction(type, action) {
|
function getViewDetailsAction(type, action) {
|
||||||
const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action));
|
const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action));
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -1997,9 +2023,8 @@ function getViewDetailsAction(type, action) {
|
||||||
}
|
}
|
||||||
case 'post': {
|
case 'post': {
|
||||||
const root = action.content && action.content.root;
|
const root = action.content && action.content.root;
|
||||||
return root
|
if (commentOf(action) || root) return commentHref(commentOf(action) || { rootId: root }, safeMsgId(action.tipId || action.id || action.key || action));
|
||||||
? `/blogs/${encodeURIComponent(root)}#${id}`
|
return `/blogs/${id}`;
|
||||||
: `/blogs/${id}`;
|
|
||||||
}
|
}
|
||||||
case 'feed': return `/feed/${id}`;
|
case 'feed': return `/feed/${id}`;
|
||||||
case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
|
case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
|
||||||
|
|
@ -2021,7 +2046,7 @@ function getViewDetailsAction(type, action) {
|
||||||
case 'forum': {
|
case 'forum': {
|
||||||
const root = action.content?.root;
|
const root = action.content?.root;
|
||||||
const rootId = typeof root === 'string' ? root : (root && (root.key || root.id)) || '';
|
const rootId = typeof root === 'string' ? root : (root && (root.key || root.id)) || '';
|
||||||
if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`;
|
if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#${forumReplyAnchor(action.tipId || action.id)}`;
|
||||||
return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
|
return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
|
||||||
}
|
}
|
||||||
case 'document': return `/documents/${id}`;
|
case 'document': return `/documents/${id}`;
|
||||||
|
|
@ -2061,6 +2086,9 @@ function getViewDetailsAction(type, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getViewDetailsAction = getViewDetailsAction;
|
exports.getViewDetailsAction = getViewDetailsAction;
|
||||||
|
exports.commentHref = commentHref;
|
||||||
|
exports.commentLabel = commentLabel;
|
||||||
|
exports.rootTypeLabel = rootTypeLabel;
|
||||||
|
|
||||||
exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
||||||
const spreadMap = (extras && extras.spreadMap) || new Map();
|
const spreadMap = (extras && extras.spreadMap) || new Map();
|
||||||
|
|
@ -2112,23 +2140,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTER_META = new Set(['all', 'mine', 'recent']);
|
const FILTER_META = new Set(['all', 'mine', 'recent']);
|
||||||
const GROUP_SUBTYPES = {
|
|
||||||
parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'],
|
|
||||||
courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'],
|
|
||||||
task: ['task', 'taskAssignment'],
|
|
||||||
votes: ['votes', 'poll'],
|
|
||||||
shop: ['shop', 'shopProduct'],
|
|
||||||
larp: ['larpHousePost'],
|
|
||||||
inhabitants:['about'],
|
|
||||||
chat: ['chat', 'chatThread'],
|
|
||||||
wiki: ['wikiPage'],
|
|
||||||
mailing: ['mailingList'],
|
|
||||||
emergency: ['emergency', 'emergencyUpdate'],
|
|
||||||
logistics: ['logisticsRoute'],
|
|
||||||
podcast: ['podcast', 'podcastEpisode'],
|
|
||||||
campaign: ['campaign', 'campaignUpdate'],
|
|
||||||
industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation']
|
|
||||||
};
|
|
||||||
const ALLOWED_TYPES = new Set(['tribeOpened']);
|
const ALLOWED_TYPES = new Set(['tribeOpened']);
|
||||||
for (const { type } of activityTypes) {
|
for (const { type } of activityTypes) {
|
||||||
if (FILTER_META.has(type)) continue;
|
if (FILTER_META.has(type)) continue;
|
||||||
|
|
@ -2185,13 +2196,18 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
||||||
filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone');
|
filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone');
|
||||||
} else {
|
} else {
|
||||||
filteredActions = actions.filter(action =>
|
filteredActions = actions.filter(action =>
|
||||||
(action.type === filter
|
((action.type === filter && !(filter === 'post' && commentOf(action) && commentOf(action).rootType !== 'post'))
|
||||||
|| filter === 'all'
|
|| filter === 'all'
|
||||||
|| openedIn(action, filter)
|
|| openedIn(action, filter)
|
||||||
|| (filter === 'shop' && action.type === 'shopProduct')
|
|| (filter === 'shop' && action.type === 'shopProduct')
|
||||||
|| (filter === 'votes' && action.type === 'poll'))
|
|| (filter === 'votes' && action.type === 'poll'))
|
||||||
&& action.type !== 'tombstone');
|
&& action.type !== 'tombstone');
|
||||||
}
|
}
|
||||||
|
if (!FILTER_META.has(filter) && filter !== 'post') {
|
||||||
|
const kinds = new Set(GROUP_SUBTYPES[filter] || [filter]);
|
||||||
|
const listed = new Set(filteredActions);
|
||||||
|
filteredActions = filteredActions.concat(actions.filter(a => !listed.has(a) && commentOf(a) && kinds.has(commentOf(a).rootType)));
|
||||||
|
}
|
||||||
|
|
||||||
const larpEarliest = new Map();
|
const larpEarliest = new Map();
|
||||||
for (const action of filteredActions) {
|
for (const action of filteredActions) {
|
||||||
|
|
@ -2332,12 +2348,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasDocument = actions.some(a => a && a.type === 'document');
|
|
||||||
if (hasDocument) {
|
|
||||||
html += `
|
|
||||||
<script type="module" src="/js/pdf-viewer.js?v=102"></script>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe");
|
const { div, h2, p, section, button, form, input, a, span } = require("../server/node_modules/hyperaxe");
|
||||||
const { safeExternalHref } = require("../backend/renderStyledText");
|
const { safeExternalHref } = require("../backend/renderStyledText");
|
||||||
const { template, i18n, userLink, renderContentActions, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views');
|
const { template, i18n, userLink, renderCardTop, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views');
|
||||||
const moment = require('../server/node_modules/moment');
|
const moment = require('../server/node_modules/moment');
|
||||||
const { config } = require('../server/SSB_server.js');
|
const { config } = require('../server/SSB_server.js');
|
||||||
|
|
||||||
|
|
@ -151,27 +151,24 @@ const renderAgendaItem = (item, userId, filter, extras = {}) => {
|
||||||
const href = getViewDetailsAction(item);
|
const href = getViewDetailsAction(item);
|
||||||
const title = item.title || item.name || item.concept || '';
|
const title = item.title || item.name || item.concept || '';
|
||||||
return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') },
|
return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') },
|
||||||
div({ class: 'card-header activity-card-header' },
|
renderCardTop(item.id, href, {
|
||||||
|
date: item.createdAt,
|
||||||
|
chips: [
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase())
|
span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase())
|
||||||
),
|
),
|
||||||
renderContentActions(item.id, href, {
|
chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null
|
||||||
|
],
|
||||||
author,
|
author,
|
||||||
reportTitle: title,
|
reportTitle: title,
|
||||||
spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined,
|
spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined,
|
||||||
...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}),
|
...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}),
|
||||||
returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`,
|
returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`,
|
||||||
deleteAction: isOwn ? agendaDeleteAction(item) : undefined
|
deleteAction: isOwn ? agendaDeleteAction(item) : undefined
|
||||||
})
|
}),
|
||||||
),
|
|
||||||
div({ class: 'card-section agenda-card-body' },
|
div({ class: 'card-section agenda-card-body' },
|
||||||
div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href }, title))),
|
div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href }, title))),
|
||||||
chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null,
|
|
||||||
...body.filter(Boolean),
|
...body.filter(Boolean),
|
||||||
p({ class: 'card-footer' },
|
|
||||||
span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm') : ''}`),
|
|
||||||
author ? userLink(author) : ''
|
|
||||||
),
|
|
||||||
div({ class: 'agenda-card-actions' }, actionButton, ...extraActions)
|
div({ class: 'agenda-card-actions' }, actionButton, ...extraActions)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue