upstream: integrar Oasis 1.3.3
Diferencia 1.3.1 -> 1.3.3 del codigo de la APK oficial aplicada a tres bandas encima de la rama: karma en el perfil, salas con directo, fotos selladas, visibilidad de menciones, telefono por stream en Android y las traducciones nuevas. Sin dependencias nuevas. OasisMobile.css sigue siendo el del fork; del oficial solo se traen al final las reglas de las piezas nuevas (karma, directo a pantalla completa, salas en vivo, graphos).
This commit is contained in:
parent
bc64565e9c
commit
948b6460d9
95 changed files with 3613 additions and 1356 deletions
|
|
@ -13,7 +13,7 @@
|
||||||
>
|
>
|
||||||
> **Los fallos de esta rama se reportan aquí, nunca a epsylon.**
|
> **Los fallos de esta rama se reportan aquí, nunca a epsylon.**
|
||||||
|
|
||||||
Versión base: **Oasis 1.3.1** · Rama: `PRUEBAS` ·
|
Versión base: **Oasis 1.3.3** · Rama: `PRUEBAS` ·
|
||||||
[Documentación completa](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki) ·
|
[Documentación completa](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki) ·
|
||||||
[Versión de escritorio](https://gitea.laenre.net/hacklab/OASIS_LINUX)
|
[Versión de escritorio](https://gitea.laenre.net/hacklab/OASIS_LINUX)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ android {
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
// el pipeline los pasa por entorno; a mano valen los de aqui
|
// el pipeline los pasa por entorno; a mano valen los de aqui
|
||||||
versionCode = (System.getenv("OASIS_VERSION_CODE") ?: "1").toInt()
|
versionCode = (System.getenv("OASIS_VERSION_CODE") ?: "1").toInt()
|
||||||
versionName = System.getenv("OASIS_VERSION_NAME") ?: "1.3.1"
|
versionName = System.getenv("OASIS_VERSION_NAME") ?: "1.3.3"
|
||||||
ndk { abiFilters += listOf("arm64-v8a") }
|
ndk { abiFilters += listOf("arm64-v8a") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ fi
|
||||||
# --- 2) backend + dependencias ---------------------------------------------
|
# --- 2) backend + dependencias ---------------------------------------------
|
||||||
# El node_modules del movil esta podado: son ~137 MB frente a los 1,4 GB de una
|
# El node_modules del movil esta podado: son ~137 MB frente a los 1,4 GB de una
|
||||||
# instalacion de escritorio. En vez de reinstalarlo, se reutiliza el del APK base:
|
# instalacion de escritorio. En vez de reinstalarlo, se reutiliza el del APK base:
|
||||||
# tiene que ser la APK oficial de la MISMA version que src/ (ahora 1.3.1): desde la
|
# tiene que ser la APK oficial de la MISMA version que src/ (ahora 1.3.3): desde la
|
||||||
# 1.1 el backend usa ssb-db2 y el node_modules de una 0.9.x ya no le sirve.
|
# 1.1 el backend usa ssb-db2 y el node_modules de una 0.9.x ya no le sirve.
|
||||||
BASE_ZIP=""
|
BASE_ZIP=""
|
||||||
if [ -n "$SRC" ] && [ -f "$SRC" ]; then
|
if [ -n "$SRC" ] && [ -f "$SRC" ]; then
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.1",
|
"version": "1.3.3",
|
||||||
"description": "Oasis - Social Networking Utopia",
|
"description": "Oasis - Social Networking Utopia",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -162,8 +162,12 @@ async function listExchanges() {
|
||||||
|
|
||||||
const netVotes = (votes, key) => { const per = votes.get(key); if (!per) return 0; let n = 0; for (const v of per.values()) n += v; return n }
|
const netVotes = (votes, key) => { const per = votes.get(key); if (!per) return 0; let n = 0; for (const v of per.values()) n += v; return n }
|
||||||
|
|
||||||
async function rankExchanges(question, { embed, cosine, k = 5 } = {}) {
|
async function rankExchanges(question, { embed, cosine, k = 5, excludeAuthors = null } = {}) {
|
||||||
const { exchanges, votes } = await listExchanges()
|
const listed = await listExchanges()
|
||||||
|
const votes = listed.votes
|
||||||
|
const exchanges = excludeAuthors instanceof Set && excludeAuthors.size
|
||||||
|
? listed.exchanges.filter(ex => !excludeAuthors.has(String(ex.author)))
|
||||||
|
: listed.exchanges
|
||||||
if (!exchanges.length) return []
|
if (!exchanges.length) return []
|
||||||
const scored = []
|
const scored = []
|
||||||
if (typeof embed === 'function' && typeof cosine === 'function') {
|
if (typeof embed === 'function' && typeof cosine === 'function') {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -222,7 +222,9 @@ const oggMime = (buffer) => {
|
||||||
return 'video/ogg';
|
return 'video/ogg';
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeUploadedFile = async function (blobUpload) {
|
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) {
|
||||||
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);
|
||||||
|
|
@ -270,14 +272,19 @@ const storeUploadedFile = async function (blobUpload) {
|
||||||
data = stripPdfMetadata(data);
|
data = stripPdfMetadata(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssbClient = await cooler.open();
|
if (typeof seal === 'function') {
|
||||||
|
await promisesFs.writeFile(blobUpload.filepath, data);
|
||||||
blob.id = await new Promise((resolve, reject) => {
|
const sealed = await seal({ filepath: blobUpload.filepath, size: data.length, name: blob.name, mime: blob.mime });
|
||||||
pull(
|
blob.id = sealedRef(sealed, blob.mime, blob.name);
|
||||||
pull.values([data]),
|
} else {
|
||||||
ssbClient.blobs.add((err, ref) => (err ? reject(err) : resolve(ref)))
|
const ssbClient = await cooler.open();
|
||||||
);
|
blob.id = await new Promise((resolve, reject) => {
|
||||||
});
|
pull(
|
||||||
|
pull.values([data]),
|
||||||
|
ssbClient.blobs.add((err, ref) => (err ? reject(err) : resolve(ref)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (blob.mime.startsWith("image/")) return `\n`;
|
if (blob.mime.startsWith("image/")) return `\n`;
|
||||||
if (blob.mime.startsWith("audio/")) return `\n[audio:${blob.name}](${blob.id})`;
|
if (blob.mime.startsWith("audio/")) return `\n[audio:${blob.name}](${blob.id})`;
|
||||||
|
|
@ -288,20 +295,20 @@ const storeUploadedFile = async function (blobUpload) {
|
||||||
return `\n[${blob.name}](${blob.id})`;
|
return `\n[${blob.name}](${blob.id})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlobUpload = async function (ctx, fileFieldName) {
|
const handleBlobUpload = async function (ctx, fileFieldName, seal = 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);
|
return storeUploadedFile(first, seal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlobUploads = async function (ctx, fileFieldName, max = 8) {
|
const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) {
|
||||||
if (!ctx.request.files || !ctx.request.files[fileFieldName]) return [];
|
if (!ctx.request.files || !ctx.request.files[fileFieldName]) return [];
|
||||||
const entry = ctx.request.files[fileFieldName];
|
const entry = ctx.request.files[fileFieldName];
|
||||||
const files = (Array.isArray(entry) ? entry : [entry]).slice(0, Math.max(0, max));
|
const files = (Array.isArray(entry) ? entry : [entry]).slice(0, Math.max(0, max));
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const stored = await storeUploadedFile(file);
|
const stored = await storeUploadedFile(file, seal);
|
||||||
if (stored) out.push(stored);
|
if (stored) out.push(stored);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,21 @@ 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 karmaModeration = [...moderation.filter(c => !karmaNeutral.includes(c)), "propaganda"];
|
||||||
|
all.karma = {
|
||||||
|
positive,
|
||||||
|
neutral: karmaNeutral,
|
||||||
|
constructive: constructive.filter(c => !karmaNeutral.includes(c) && !karmaModeration.includes(c)),
|
||||||
|
moderation: karmaModeration
|
||||||
|
};
|
||||||
all.messageTypes = {
|
all.messageTypes = {
|
||||||
bookmark: 'bookmarkOpinion',
|
bookmark: 'bookmarkOpinion',
|
||||||
votes: 'votesOpinion',
|
votes: 'votesOpinion',
|
||||||
poll: 'pollOpinion',
|
poll: 'pollOpinion',
|
||||||
transfer: 'transferOpinion',
|
transfer: 'transferOpinion',
|
||||||
feed: 'feedOpinion',
|
feed: 'feedOpinion',
|
||||||
|
post: 'blogOpinion',
|
||||||
image: 'imageOpinion',
|
image: 'imageOpinion',
|
||||||
audio: 'audioOpinion',
|
audio: 'audioOpinion',
|
||||||
video: 'videoOpinion',
|
video: 'videoOpinion',
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ const isMutual = (relationship) => !!(relationship && relationship.following &&
|
||||||
|
|
||||||
const isRecipientAllowed = ({ pmVisibility, viewerId, recipientId, relationship } = {}) => {
|
const isRecipientAllowed = ({ pmVisibility, viewerId, recipientId, relationship } = {}) => {
|
||||||
if (recipientId && viewerId && recipientId === viewerId) return true;
|
if (recipientId && viewerId && recipientId === viewerId) return true;
|
||||||
if (pmVisibility !== 'mutuals') return true;
|
if (pmVisibility === 'mutuals') return isMutual(relationship);
|
||||||
return isMutual(relationship);
|
if (pmVisibility === 'following') return !!(relationship && relationship.following);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const announceSeenPath = () => {
|
const announceSeenPath = () => {
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ const UNDERLINE_RE = /(?<![A-Za-z0-9])__(?!\s)((?:[^_\n]|_(?!_))+?)(?<!\s)__(?![
|
||||||
const STRIKE_RE = /~~(?!\s)([^~\n]+?)(?<!\s)~~/g;
|
const STRIKE_RE = /~~(?!\s)([^~\n]+?)(?<!\s)~~/g;
|
||||||
const ITALIC_RE = /\*(?!\s)((?:[^*\n]|\*\*)+?)(?<!\s)\*(?!\*)/g;
|
const ITALIC_RE = /\*(?!\s)((?:[^*\n]|\*\*)+?)(?<!\s)\*(?!\*)/g;
|
||||||
const INLINE_CODE_RE = /`([^`\n]+)`/g;
|
const INLINE_CODE_RE = /`([^`\n]+)`/g;
|
||||||
const BLOB_IMAGE_RE = /!\[([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256)\s*\)/g;
|
const BLOB_IMAGE_RE = /!\[([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const BLOB_VIDEO_RE = /\[video:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256)\s*\)/g;
|
const BLOB_VIDEO_RE = /\[video:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const BLOB_AUDIO_RE = /\[audio:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256)\s*\)/g;
|
const BLOB_AUDIO_RE = /\[audio:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const BLOB_PDF_RE = /\[pdf:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256)\s*\)/g;
|
const BLOB_PDF_RE = /\[pdf:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const BLOB_TORRENT_RE = /\[torrent:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256)\s*\)/g;
|
const BLOB_TORRENT_RE = /\[torrent:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const BLOB_FILE_RE = /(?<!!)\[(?!(?:video|audio|pdf|torrent):)([^\]\n]{1,160})\]\(\s*(&[^)\s]+\.sha256)\s*\)/g;
|
const BLOB_FILE_RE = /(?<!!)\[(?!(?:video|audio|pdf|torrent):)([^\]\n]{1,160})\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
||||||
const MD_MENTION_RE = /\[@([^\]\[\n]{1,120})\]\(@?([A-Za-z0-9+/=.\-]{1,120}\.ed25519)\)/g;
|
const MD_MENTION_RE = /\[@([^\]\[\n]{1,120})\]\(@?([A-Za-z0-9+/=.\-]{1,120}\.ed25519)\)/g;
|
||||||
const RAW_MENTION_RE = /@([A-Za-z0-9+/=.\-]+\.ed25519)/g;
|
const RAW_MENTION_RE = /@([A-Za-z0-9+/=.\-]+\.ed25519)/g;
|
||||||
const BARE_MENTION_RE = /(^|[^\w\u0080-\uFFFF\[\]/@])@([\w\u0080-\uFFFF.\-]{1,64})(?=$|[^\w\u0080-\uFFFF/+=])/g;
|
const BARE_MENTION_RE = /(^|[^\w\u0080-\uFFFF\[\]/@])@([\w\u0080-\uFFFF.\-]{1,64})(?=$|[^\w\u0080-\uFFFF/+=])/g;
|
||||||
|
|
@ -141,7 +141,7 @@ function renderStyledText(value, opts = {}) {
|
||||||
filtered.push(m);
|
filtered.push(m);
|
||||||
lastEnd = m.index + m.length;
|
lastEnd = m.index + m.length;
|
||||||
}
|
}
|
||||||
const blobHref = (id) => `${opts.blobPrefix || '/blob/'}${encodeURIComponent(id)}`;
|
const blobHref = (id) => String(id).startsWith('/blob/sealed/') ? String(id) : `${opts.blobPrefix || '/blob/'}${encodeURIComponent(id)}`;
|
||||||
const linksOn = opts.links !== false;
|
const linksOn = opts.links !== false;
|
||||||
const internalOn = opts.internalLinks !== false;
|
const internalOn = opts.internalLinks !== false;
|
||||||
const plain = (value) => opts.plainUrlClass ? span({ class: opts.plainUrlClass }, value) : value;
|
const plain = (value) => opts.plainUrlClass ? span({ class: opts.plainUrlClass }, value) : value;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const SAFE_SRC = /^\/(?:blob|image)\/[^\s"'<>]+$/i;
|
const SAFE_SRC = /^\/(?:blob|image)\/[^\s"'<>]+$/i;
|
||||||
const SAFE_HREF = /^(?:https?:\/\/|\/(?!\/)|#|mailto:)/i;
|
const SAFE_HREF = /^(?:https?:\/\/|\/(?!\/)|#|mailto:)/i;
|
||||||
const SAFE_CLASS = /^(?:rt-[a-z0-9-]+|styled-link|mention|tag-link|zoom-link|pm-quote|wiki-link(?:-missing)?|internal-link(?:-id|-kind)?|lightbox(?:-[a-z]+)?|post-(?:audio|image|pdf|video))$/;
|
const SAFE_CLASS = /^(?:rt-[a-z0-9-]+|styled-link|mention|tag-link|tag-inline|zoom-link|pm-quote|wiki-link(?:-missing)?|internal-link(?:-id|-kind)?|lightbox(?:-[a-z]+)?|post-(?:audio|image|pdf|video))$/;
|
||||||
let purifier = null;
|
let purifier = null;
|
||||||
const getPurify = () => {
|
const getPurify = () => {
|
||||||
if (purifier) return purifier;
|
if (purifier) return purifier;
|
||||||
|
|
|
||||||
BIN
src/client/assets/images/ux-rooms.png
Normal file
BIN
src/client/assets/images/ux-rooms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -842,6 +842,7 @@ a.create-button {
|
||||||
|
|
||||||
.peers-list .block-info-table,
|
.peers-list .block-info-table,
|
||||||
.peers-technical-block .block-info-table,
|
.peers-technical-block .block-info-table,
|
||||||
|
.viewer-hidden-block .block-info-table,
|
||||||
.parliament-rows-table,
|
.parliament-rows-table,
|
||||||
.gov-overview {
|
.gov-overview {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
@ -853,6 +854,7 @@ a.create-button {
|
||||||
}
|
}
|
||||||
.peers-list .block-info-table tbody,
|
.peers-list .block-info-table tbody,
|
||||||
.peers-technical-block .block-info-table tbody,
|
.peers-technical-block .block-info-table tbody,
|
||||||
|
.viewer-hidden-block .block-info-table tbody,
|
||||||
.parliament-rows-table tbody,
|
.parliament-rows-table tbody,
|
||||||
.gov-overview tbody {
|
.gov-overview tbody {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
@ -861,11 +863,13 @@ a.create-button {
|
||||||
.gov-overview thead,
|
.gov-overview thead,
|
||||||
.parliament-rows-table thead { display: none !important; }
|
.parliament-rows-table thead { display: none !important; }
|
||||||
.peers-list .block-info-table tr:first-child,
|
.peers-list .block-info-table tr:first-child,
|
||||||
.peers-technical-block .block-info-table tr:first-child {
|
.peers-technical-block .block-info-table tr:first-child,
|
||||||
|
.viewer-hidden-block .block-info-table tr:first-child {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.peers-list .block-info-table tr,
|
.peers-list .block-info-table tr,
|
||||||
.peers-technical-block .block-info-table tr,
|
.peers-technical-block .block-info-table tr,
|
||||||
|
.viewer-hidden-block .block-info-table tr,
|
||||||
.parliament-rows-table tbody tr,
|
.parliament-rows-table tbody tr,
|
||||||
.gov-overview tbody tr {
|
.gov-overview tbody tr {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
@ -879,6 +883,7 @@ a.create-button {
|
||||||
}
|
}
|
||||||
.peers-list .block-info-table td,
|
.peers-list .block-info-table td,
|
||||||
.peers-technical-block .block-info-table td,
|
.peers-technical-block .block-info-table td,
|
||||||
|
.viewer-hidden-block .block-info-table td,
|
||||||
.parliament-rows-table tbody td,
|
.parliament-rows-table tbody td,
|
||||||
.gov-overview tbody td {
|
.gov-overview tbody td {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
|
@ -895,6 +900,7 @@ a.create-button {
|
||||||
}
|
}
|
||||||
.peers-list .block-info-table td::before,
|
.peers-list .block-info-table td::before,
|
||||||
.peers-technical-block .block-info-table td::before,
|
.peers-technical-block .block-info-table td::before,
|
||||||
|
.viewer-hidden-block .block-info-table td::before,
|
||||||
.parliament-rows-table tbody td::before,
|
.parliament-rows-table tbody td::before,
|
||||||
.gov-overview tbody td::before {
|
.gov-overview tbody td::before {
|
||||||
content: attr(data-label);
|
content: attr(data-label);
|
||||||
|
|
@ -908,8 +914,10 @@ a.create-button {
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
word-break: normal !important;
|
word-break: normal !important;
|
||||||
}
|
}
|
||||||
.peers-technical-block .block-info-table td[data-label=""] { justify-content: center !important; }
|
.peers-technical-block .block-info-table td[data-label=""],
|
||||||
.peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; }
|
.viewer-hidden-block .block-info-table td[data-label=""] { justify-content: center !important; }
|
||||||
|
.peers-technical-block .block-info-table td[data-label=""]::before,
|
||||||
|
.viewer-hidden-block .block-info-table td[data-label=""]::before { content: none !important; }
|
||||||
.peers-list .block-info-table td .peer-key,
|
.peers-list .block-info-table td .peer-key,
|
||||||
.peers-technical-block .block-info-table td .peer-key { text-align: right !important; }
|
.peers-technical-block .block-info-table td .peer-key { text-align: right !important; }
|
||||||
.phone-history td.phone-dir,
|
.phone-history td.phone-dir,
|
||||||
|
|
|
||||||
|
|
@ -1042,6 +1042,11 @@ article img {
|
||||||
.feed-container .post-image{max-width:100%;width:auto;max-height:480px;height:auto}
|
.feed-container .post-image{max-width:100%;width:auto;max-height:480px;height:auto}
|
||||||
.feed-container .post-video,.feed-container .video-container video{width:100%;max-width:720px;max-height:480px;height:auto}
|
.feed-container .post-video,.feed-container .video-container video{width:100%;max-width:720px;max-height:480px;height:auto}
|
||||||
.feed-container .post-audio,.feed-container .audio-container audio{width:100%}
|
.feed-container .post-audio,.feed-container .audio-container audio{width:100%}
|
||||||
|
.feed-detail-card .post-image{width:auto;max-height:480px;margin-left:auto;margin-right:auto}
|
||||||
|
.feed-detail-card .post-video{max-height:480px;margin-left:auto;margin-right:auto}
|
||||||
|
.feed-detail-card .post-audio{width:100%}
|
||||||
|
.feed-detail-card .zoom-link:not(.zoom-inline){display:block;text-align:center}
|
||||||
|
.feed-detail-card .zoom-link:not(.zoom-inline) img{display:inline-block}
|
||||||
|
|
||||||
.fediverse-network{border-radius:8px;padding:18px 22px;margin-top:10px;display:flex;flex-direction:column;gap:16px}
|
.fediverse-network{border-radius:8px;padding:18px 22px;margin-top:10px;display:flex;flex-direction:column;gap:16px}
|
||||||
.fediverse-network > p,.fediverse-network form > p{margin:0}
|
.fediverse-network > p,.fediverse-network form > p{margin:0}
|
||||||
|
|
@ -1521,6 +1526,7 @@ button.create-button:hover {
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.karma-line-main{display:block;line-height:1.25}.profile-sensors-box .karma-line.karma-line-stacked,.inhabitant-left .karma-line.karma-line-stacked,.inhabitant-karma-ubi .karma-line.karma-line-stacked,.karma-line.karma-line-stacked{display:flex;flex-direction:column;align-items:center;gap:1px}.karma-received-row{display:flex;justify-content:center;align-items:center;gap:14px;margin-top:0;line-height:1.25}.karma-received{display:inline-flex;align-items:center;gap:4px;font-weight:600;white-space:nowrap;line-height:1}.karma-arrow{font-size:.85em;line-height:1;display:inline-block}.karma-received-up{color:#2ecc71}.karma-received-down{color:#e74c3c}.karma-value-positive{color:#2ecc71 !important}.karma-value-neutral{color:#ffd700 !important}.karma-value-negative{color:#e74c3c !important}
|
||||||
.module-stats-line .stat-label-green{color:#2ecc71}
|
.module-stats-line .stat-label-green{color:#2ecc71}
|
||||||
.module-stats-line .stat-label-orange{color:#f39c12}
|
.module-stats-line .stat-label-orange{color:#f39c12}
|
||||||
.module-stats-line .stat-label-red{color:#e74c3c}
|
.module-stats-line .stat-label-red{color:#e74c3c}
|
||||||
|
|
@ -2508,6 +2514,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-main a.tag-link,
|
||||||
|
.feed-main a.tag-link:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.votes-wrapper {
|
.votes-wrapper {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
|
|
@ -6005,6 +6016,12 @@ a:hover .map-cluster-dot { fill: #ffffff; }
|
||||||
.chat-pin-toggle{position:absolute;top:4px;right:6px}
|
.chat-pin-toggle{position:absolute;top:4px;right:6px}
|
||||||
.chat-has-pin-toggle{padding-right:38px}
|
.chat-has-pin-toggle{padding-right:38px}
|
||||||
.chat-pinned-link{margin-right:8px}
|
.chat-pinned-link{margin-right:8px}
|
||||||
|
.btn-singleview .btn-icon{display:flex;align-items:center;justify-content:center;width:1em;height:1em;line-height:0}
|
||||||
|
.btn-singleview .btn-icon svg{display:block;width:100%;height:100%}
|
||||||
|
.btn-singleview .btn-count{margin-left:5px;line-height:1;font-size:.9em}
|
||||||
|
.btn-singleview:has(> .btn-icon:only-child){width:36px;min-width:36px;height:36px;padding:0;flex-shrink:0}
|
||||||
|
.btn-singleview.btn-content{background:#ffa300;color:#1e1f23;border:1px solid #ffa300}
|
||||||
|
.btn-singleview.btn-content:hover{background:#FFD700;color:#1e1f23;border-color:#FFD700}
|
||||||
.btn-singleview.btn-delete{color:#ff6b6b !important;border-color:rgba(255,107,107,.55) !important}
|
.btn-singleview.btn-delete{color:#ff6b6b !important;border-color:rgba(255,107,107,.55) !important}
|
||||||
.btn-singleview.btn-delete:hover{background:rgba(255,107,107,.15) !important}
|
.btn-singleview.btn-delete:hover{background:rgba(255,107,107,.15) !important}
|
||||||
.feed-opinions-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0;box-shadow:none}
|
.feed-opinions-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0;box-shadow:none}
|
||||||
|
|
@ -6209,8 +6226,8 @@ details[open] > .school-lesson-summary .school-lesson-summary-title::after{conte
|
||||||
.torrent-table{width:100%;border-collapse:collapse;margin-top:12px}
|
.torrent-table{width:100%;border-collapse:collapse;margin-top:12px}
|
||||||
.torrent-table th,.torrent-table td{padding:8px 12px;text-align:left}
|
.torrent-table th,.torrent-table td{padding:8px 12px;text-align:left}
|
||||||
.torrent-table td:has(.filter-btn){width:1%;white-space:nowrap}
|
.torrent-table td:has(.filter-btn){width:1%;white-space:nowrap}
|
||||||
.torrent-table td:nth-child(3){max-width:380px;overflow-wrap:anywhere}
|
.torrent-table td:nth-child(2){width:auto;overflow-wrap:anywhere}
|
||||||
.torrent-table td:nth-child(4),.torrent-table td:nth-child(6){white-space:nowrap;width:1%}
|
.torrent-table th:nth-child(1),.torrent-table td:nth-child(1),.torrent-table td:nth-child(3),.torrent-table td:nth-child(4),.torrent-table td:nth-child(5){white-space:nowrap;width:1%}
|
||||||
.torrent-table td .filter-btn{margin:0}
|
.torrent-table td .filter-btn{margin:0}
|
||||||
.torrent-table th{background:#1a1a1a;color:#FFA500;font-weight:bold}
|
.torrent-table th{background:#1a1a1a;color:#FFA500;font-weight:bold}
|
||||||
.torrent-download{margin:12px 0;background:transparent;border:0;padding:0;box-shadow:none}
|
.torrent-download{margin:12px 0;background:transparent;border:0;padding:0;box-shadow:none}
|
||||||
|
|
@ -6318,6 +6335,7 @@ details[open] > .school-lesson-summary .school-lesson-summary-title::after{conte
|
||||||
.stats-toplist .stats-toplist-num{flex:0 0 auto;color:#FFDD44;font-size:13px;min-width:36px;text-align:right}
|
.stats-toplist .stats-toplist-num{flex:0 0 auto;color:#FFDD44;font-size:13px;min-width:36px;text-align:right}
|
||||||
.peer-key{word-break:break-all;font-size:12px}
|
.peer-key{word-break:break-all;font-size:12px}
|
||||||
.peers-list .block-info-table{table-layout:fixed;width:100%}
|
.peers-list .block-info-table{table-layout:fixed;width:100%}
|
||||||
|
.viewer-hidden-block .block-info-table{width:100%}.viewer-hidden-block .block-info-table td{overflow-wrap:anywhere}.viewer-hidden-block .block-info-table td:nth-child(2),.viewer-hidden-block .block-info-table td:nth-child(3){white-space:nowrap;width:1%}
|
||||||
.peers-list .block-info-table td{overflow-wrap:anywhere}
|
.peers-list .block-info-table td{overflow-wrap:anywhere}
|
||||||
.peers-list .block-info-table td:nth-child(1){width:18%}
|
.peers-list .block-info-table td:nth-child(1){width:18%}
|
||||||
.peers-list .block-info-table td:nth-child(2){width:47%}
|
.peers-list .block-info-table td:nth-child(2){width:47%}
|
||||||
|
|
@ -6326,6 +6344,8 @@ details[open] > .school-lesson-summary .school-lesson-summary-title::after{conte
|
||||||
.peers-list .block-info-table td:nth-child(5){width:15%}
|
.peers-list .block-info-table td:nth-child(5){width:15%}
|
||||||
.peers-technical-block{margin-top:16px}
|
.peers-technical-block{margin-top:16px}
|
||||||
.peers-conn-actions{margin-bottom:12px}
|
.peers-conn-actions{margin-bottom:12px}
|
||||||
|
.activity-toolbar-row .peers-conn-actions{margin:0;padding:0;border:0;background:transparent;box-shadow:none;display:flex;gap:6px;flex-wrap:wrap;margin-left:auto}
|
||||||
|
.activity-toolbar-row .peers-conn-actions form{margin:0}
|
||||||
.invites-pubs-actions{margin:12px 0}
|
.invites-pubs-actions{margin:12px 0}
|
||||||
.larp-cycle-banner{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:#1A1A1A;border:1px solid #FFA500;border-radius:6px;padding:8px 14px;margin:12px 0;font-family:monospace}
|
.larp-cycle-banner{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:#1A1A1A;border:1px solid #FFA500;border-radius:6px;padding:8px 14px;margin:12px 0;font-family:monospace}
|
||||||
.larp-post-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start}
|
.larp-post-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start}
|
||||||
|
|
@ -8088,7 +8108,6 @@ details[open] > .chat-poll-summary::after { content: " \25B4"; }
|
||||||
.blockexplorer-search-actions input[type="text"],
|
.blockexplorer-search-actions input[type="text"],
|
||||||
.logs-toolbar input[type="text"],
|
.logs-toolbar input[type="text"],
|
||||||
.search-filters-row input[type="text"],
|
.search-filters-row input[type="text"],
|
||||||
.tribe-search-form input[type="text"],
|
|
||||||
.filter-box input[type="text"],
|
.filter-box input[type="text"],
|
||||||
.ui-toolbar input[type="text"] {
|
.ui-toolbar input[type="text"] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -8327,7 +8346,9 @@ details[open] > .chat-poll-summary::after { content: " \25B4"; }
|
||||||
.school-progress-failed{font-size:0.85em;color:#e57373;cursor:help}
|
.school-progress-failed{font-size:0.85em;color:#e57373;cursor:help}
|
||||||
.school-lessons .school-lesson,.school-exams .school-lesson{background:transparent !important;border:0 !important;box-shadow:none;padding:0 !important;margin:0 0 16px 0}
|
.school-lessons .school-lesson,.school-exams .school-lesson{background:transparent !important;border:0 !important;box-shadow:none;padding:0 !important;margin:0 0 16px 0}
|
||||||
.school-exam-question{background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0 0 14px 0}
|
.school-exam-question{background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0 0 14px 0}
|
||||||
.top-bar-center .ai-ask-form{flex:1 1 auto;min-width:220px}
|
.top-bar-center .ai-ask-form{flex:1 1 auto;min-width:0}
|
||||||
|
.top-bar-center:not(.top-bar-center-ainav){container-type:inline-size}
|
||||||
|
@container (max-width:180px){.ai-ask-row .ai-ask-form{display:none}}
|
||||||
.ai-suggestion-inline{display:inline-flex;align-items:center;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:0 0 0 12px !important;box-shadow:none;font-size:.85em;white-space:nowrap;flex:0 1 auto;min-width:0;overflow:hidden}
|
.ai-suggestion-inline{display:inline-flex;align-items:center;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:0 0 0 12px !important;box-shadow:none;font-size:.85em;white-space:nowrap;flex:0 1 auto;min-width:0;overflow:hidden}
|
||||||
.ai-suggestion-inline .update-banner-link{display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
|
.ai-suggestion-inline .update-banner-link{display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
|
||||||
.ai-suggestion-inline .welcome-banner-close{margin:0;padding:0;background:transparent;border:0;box-shadow:none;display:inline}
|
.ai-suggestion-inline .welcome-banner-close{margin:0;padding:0;background:transparent;border:0;box-shadow:none;display:inline}
|
||||||
|
|
@ -8694,7 +8715,7 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.peertube-description{white-space:pre-wrap;word-break:break-word}
|
.peertube-description{white-space:pre-wrap;word-break:break-word}
|
||||||
.peertube-nostream p{margin:8px 0 0}
|
.peertube-nostream p{margin:8px 0 0}
|
||||||
.torrent-table td{vertical-align:middle}
|
.torrent-table td{vertical-align:middle}
|
||||||
.torrent-spread-cell{text-align:center;white-space:nowrap}
|
.torrent-spread-cell,.torrent-table th.torrent-spread-cell,.torrent-table td.torrent-spread-cell{text-align:center;white-space:nowrap}
|
||||||
.torrent-spread-cell .spread-form{display:inline-block;margin:0;vertical-align:middle}
|
.torrent-spread-cell .spread-form{display:inline-block;margin:0;vertical-align:middle}
|
||||||
.torrent-spread-cell .btn-singleview{margin:0;vertical-align:middle}
|
.torrent-spread-cell .btn-singleview{margin:0;vertical-align:middle}
|
||||||
.torrent-detail-section{margin:0 0 18px}
|
.torrent-detail-section{margin:0 0 18px}
|
||||||
|
|
@ -8743,6 +8764,7 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.room-peer .phone-action-form .tribe-action-btn{padding:2px 8px;font-size:11px}
|
.room-peer .phone-action-form .tribe-action-btn{padding:2px 8px;font-size:11px}
|
||||||
.phone-call-peers{margin:8px 0 0 0}
|
.phone-call-peers{margin:8px 0 0 0}
|
||||||
.block-info-table .pm-exposition-chip{white-space:nowrap}
|
.block-info-table .pm-exposition-chip{white-space:nowrap}
|
||||||
|
.torrent-table td > .pm-exposition-chip-link,.torrent-table td > .pm-exposition-chip,.file-table td > .pm-exposition-chip-link,.file-table td > .pm-exposition-chip{margin-left:10px;vertical-align:middle;position:relative;top:-3px}
|
||||||
.peers-technical-block .inline-form .filter-btn{white-space:nowrap}
|
.peers-technical-block .inline-form .filter-btn{white-space:nowrap}
|
||||||
.peers-technical-block .peer-port{white-space:nowrap}
|
.peers-technical-block .peer-port{white-space:nowrap}
|
||||||
.peers-technical-block .peer-host{overflow-wrap:normal;word-break:normal}
|
.peers-technical-block .peer-host{overflow-wrap:normal;word-break:normal}
|
||||||
|
|
@ -8836,7 +8858,7 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.oasis-pager-btn:hover,.oasis-pager-btn.active{border-color:#FFD700;color:#FFD700;text-decoration:none}
|
.oasis-pager-btn:hover,.oasis-pager-btn.active{border-color:#FFD700;color:#FFD700;text-decoration:none}
|
||||||
.card-title-link{color:inherit;text-decoration:none}
|
.card-title-link{color:inherit;text-decoration:none}
|
||||||
.card-title-link:hover{text-decoration:underline}
|
.card-title-link:hover{text-decoration:underline}
|
||||||
.graphos-zoom-row{display:flex;justify-content:flex-end;margin:0 0 8px;padding:0;border:0;background:none;box-shadow:none}
|
.graphos-statsbar{margin:12px 0 14px}.graphos-canvas{position:relative}.graphos-canvas-svg{margin:0;padding:0;border:0 !important;background:transparent !important;box-shadow:none}.graphos-canvas .graphos-zoom-btn{position:absolute;top:8px;right:8px;margin:0;white-space:nowrap;z-index:1}.graphos-kpi .stats-kpi-label{text-transform:uppercase;white-space:nowrap}.graphos-kpi .stats-kpi-value{font-weight:bold}
|
||||||
.graphos-lightbox .graphos-canvas-zoom{width:96vw;height:92vh;overflow:auto;background:#111;border:1px solid #2a2a2a;border-radius:8px;box-sizing:border-box;padding:0;margin:0}
|
.graphos-lightbox .graphos-canvas-zoom{width:96vw;height:92vh;overflow:auto;background:#111;border:1px solid #2a2a2a;border-radius:8px;box-sizing:border-box;padding:0;margin:0}
|
||||||
.graphos-lightbox .graphos-zoom-stage,.graphos-lightbox .graphos-zoom-svg{padding:0;margin:0;border:0;border-radius:0;background:none;box-shadow:none}
|
.graphos-lightbox .graphos-zoom-stage,.graphos-lightbox .graphos-zoom-svg{padding:0;margin:0;border:0;border-radius:0;background:none;box-shadow:none}
|
||||||
.graphos-lightbox .graphos-svg{width:100%;height:auto;max-height:calc(92vh - 4px);min-height:0;display:block}
|
.graphos-lightbox .graphos-svg{width:100%;height:auto;max-height:calc(92vh - 4px);min-height:0;display:block}
|
||||||
|
|
@ -8848,24 +8870,46 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.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}
|
||||||
.stats-block .tag-table th:first-child,.stats-block .tag-table td:first-child,.stats-card .tag-table th:first-child,.stats-card .tag-table td:first-child{width:70%}
|
.stats-block .tag-table th:first-child,.stats-block .tag-table td:first-child,.stats-card .tag-table th:first-child,.stats-card .tag-table td:first-child{width:70%}
|
||||||
.stats-block .tag-table th:last-child,.stats-block .tag-table td:last-child,.stats-card .tag-table th:last-child,.stats-card .tag-table td:last-child{width:30%;text-align:right}
|
.stats-block .tag-table th:last-child,.stats-block .tag-table td:last-child,.stats-card .tag-table th:last-child,.stats-card .tag-table td:last-child{width:30%;text-align:right}
|
||||||
.video-grid{display:flex;flex-wrap:wrap;gap:8px;margin:18px 0 0;padding:0;border:0;background:transparent;box-shadow:none}
|
.video-grid{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin:18px 0 0;padding:0;border:0;background:transparent;box-shadow:none}
|
||||||
.video-tile{display:flex;flex-direction:column;gap:0;padding:0;margin:0;width:320px;max-width:100%;border:1px solid #333;border-radius:6px;overflow:hidden;background:#000}
|
.video-tile{display:flex;flex-direction:column;gap:0;padding:0;margin:0;width:420px;max-width:100%;border:1px solid #333;border-radius:6px;overflow:hidden;background:#000}
|
||||||
|
.video-grid-solo .video-tile{width:800px}
|
||||||
.video-feed{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;background:#000}
|
.video-feed{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;background:#000}
|
||||||
.video-tile .user-link,.video-tile-name{display:block;margin:0;padding:4px 8px;font-size:12px;text-align:left;background:#1a1a1a}
|
.video-tile .user-link,.video-tile-name{display:block;margin:0;padding:4px 8px;font-size:12px;text-align:left;background:#1a1a1a}
|
||||||
.video-tile-me .video-feed{transform:scaleX(-1)}
|
.video-tile-me .video-feed{transform:scaleX(-1)}
|
||||||
.room-peer-cam{flex:0 0 auto;color:#ffa300;font-size:11px;font-weight:700;border:1px solid #ffa300;border-radius:4px;padding:1px 6px}
|
.room-peer-cam{flex:0 0 auto;color:#ffa300;font-size:11px;font-weight:700;border:1px solid #ffa300;border-radius:4px;padding:1px 6px}
|
||||||
.room-cam-btn-on{background:#ffa500;color:#000}
|
.tribe-action-btn.room-cam-btn-on{background:#ffa500 !important;color:#000 !important;border-color:#ffa500 !important}
|
||||||
.video-grid-one{flex-direction:column;align-items:flex-start}
|
.room-stream-chip{color:#ffa300;font-size:11px;font-weight:700;border:1px solid #ffa300;border-radius:4px;padding:1px 6px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.video-feed-grid{width:auto;max-width:100%;aspect-ratio:auto;border:1px solid #333;border-radius:6px}
|
.room-public-warning{margin:8px 0 0;font-size:12px;color:#ffa300}
|
||||||
|
.video-tile-film{color:#ffa300}
|
||||||
|
.room-stream-form{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;position:relative}
|
||||||
|
.room-live-controls>.room-stream-form{flex:0 1 auto;max-width:100%}
|
||||||
|
.room-stream-input{position:absolute;left:0;top:0;width:1px;height:1px;opacity:0;pointer-events:none;clip-path:inset(50%)}
|
||||||
|
.room-stream-pick{cursor:pointer}
|
||||||
|
.room-stream-input:invalid ~ .room-stream-go,.room-stream-input:valid ~ .room-stream-pick{display:none}
|
||||||
|
.room-stream-go{order:2}
|
||||||
|
.room-stream-form .room-stream-input:valid{position:static !important;width:auto !important;height:auto !important;max-width:160px !important;opacity:1 !important;pointer-events:auto !important;clip-path:none !important;order:1;margin:0 !important;padding:1px 6px !important;border:1px solid #ffa300 !important;border-radius:4px !important;background:transparent !important;color:#ffa300 !important;font-size:11px !important;font-weight:700;line-height:1.6;cursor:pointer}
|
||||||
|
.room-stream-input::file-selector-button{display:none}
|
||||||
|
.room-stream-input::-webkit-file-upload-button{display:none}
|
||||||
|
.stream-picker-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.stream-picker-item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px;border:1px solid rgba(128,128,128,.4);border-radius:6px}
|
||||||
|
.stream-picker-link{display:flex;align-items:center;gap:8px;min-width:0;text-decoration:none}
|
||||||
|
.stream-picker-link .btn-icon{display:flex;flex:0 0 auto;width:18px;height:18px}
|
||||||
|
.stream-picker-link .btn-icon svg{display:block;width:100%;height:100%}
|
||||||
|
.stream-picker-name{overflow-wrap:anywhere}
|
||||||
|
.stream-picker-size{flex:0 0 auto;font-size:12px;opacity:.7;white-space:nowrap}
|
||||||
|
.stream-picker-item .phone-action-form{flex:0 0 auto;margin:0}
|
||||||
|
.video-grid-one{flex-direction:column;align-items:center}
|
||||||
|
.video-feed-grid{width:100%;max-width:100%;aspect-ratio:auto;border:1px solid #333;border-radius:6px}
|
||||||
|
.video-grid-solo .video-feed-grid{max-width:800px;margin:0 auto}
|
||||||
.video-grid-names{display:flex;flex-wrap:wrap;gap:6px;padding:0;margin:0;border:0;background:transparent;box-shadow:none}
|
.video-grid-names{display:flex;flex-wrap:wrap;gap:6px;padding:0;margin:0;border:0;background:transparent;box-shadow:none}
|
||||||
.video-grid-names .user-link,.video-grid-names .video-tile-name{padding:4px 8px;font-size:12px;background:#1a1a1a;border-radius:4px}
|
.video-grid-names .user-link,.video-grid-names .video-tile-name{padding:4px 8px;font-size:12px;background:#1a1a1a;border-radius:4px}
|
||||||
.file-table{width:100%;border-collapse:collapse;margin-top:12px}
|
.file-table{width:100%;border-collapse:collapse;margin-top:12px}
|
||||||
.file-table th,.file-table td{padding:8px 12px;text-align:left;vertical-align:middle}
|
.file-table th,.file-table td{padding:8px 12px;text-align:left;vertical-align:middle}
|
||||||
.file-table th{background:#1a1a1a;color:#FFA500;font-weight:bold}
|
.file-table th{background:#1a1a1a;color:#FFA500;font-weight:bold}
|
||||||
.file-table td:nth-child(1),.file-table td:nth-child(4),.file-table td:nth-child(5),.file-table td:nth-child(6),.file-table td:nth-child(7){white-space:nowrap;width:1%}
|
.file-table th:nth-child(1),.file-table td:nth-child(1),.file-table td:nth-child(3),.file-table td:nth-child(4),.file-table td:nth-child(5),.file-table td:nth-child(6){white-space:nowrap;width:1%}
|
||||||
.file-table td:nth-child(3){overflow-wrap:anywhere}
|
.file-table td:nth-child(2){width:auto;overflow-wrap:anywhere}
|
||||||
.file-table td .filter-btn{margin:0}
|
.file-table td .filter-btn{margin:0}
|
||||||
.file-table td.torrent-spread-cell{text-align:center}
|
.file-table th.torrent-spread-cell,.file-table td.torrent-spread-cell{text-align:center}
|
||||||
.forum-inline-reply{padding:0;margin:6px 0 0;border:0;background:transparent;box-shadow:none}
|
.forum-inline-reply{padding:0;margin:6px 0 0;border:0;background:transparent;box-shadow:none}
|
||||||
.forum-inline-reply-toggle{cursor:pointer;display:inline-block;padding:3px 10px;border:1px solid #ffa500;border-radius:10px;background:#1a1a1a;color:#ffa500;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;list-style:none;width:max-content}
|
.forum-inline-reply-toggle{cursor:pointer;display:inline-block;padding:3px 10px;border:1px solid #ffa500;border-radius:10px;background:#1a1a1a;color:#ffa500;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;list-style:none;width:max-content}
|
||||||
.forum-inline-reply-toggle::-webkit-details-marker{display:none}
|
.forum-inline-reply-toggle::-webkit-details-marker{display:none}
|
||||||
|
|
@ -8874,3 +8918,9 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0
|
||||||
.video-tile-me .lightbox-image{transform:scaleX(-1)}
|
.video-tile-me .lightbox-image{transform:scaleX(-1)}
|
||||||
.phone-call-panel .live-lightbox{background:rgba(0,0,0,.85)}
|
.phone-call-panel .live-lightbox{background:rgba(0,0,0,.85)}
|
||||||
.live-lightbox .lightbox-image{width:min(80vw,880px);height:auto;max-width:none;max-height:80vh;object-fit:contain;cursor:zoom-out}
|
.live-lightbox .lightbox-image{width:min(80vw,880px);height:auto;max-width:none;max-height:80vh;object-fit:contain;cursor:zoom-out}
|
||||||
|
.live-fullscreen-btn{position:absolute;top:15px;right:112px;width:42px;height:42px;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:rgba(0,0,0,.6);border-radius:50%;color:#fff;text-decoration:none}
|
||||||
|
.live-fullscreen-btn .btn-icon{display:flex;width:20px;height:20px}
|
||||||
|
.live-fullscreen-btn .btn-icon svg{display:block;width:100%;height:100%}
|
||||||
|
.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{width:100vw;height:100vh;max-width:100vw;max-height:100vh;object-fit:contain;border-radius:0}
|
||||||
|
|
|
||||||
|
|
@ -318,8 +318,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-singleview:hover { background: #E0E0E0 !important; color: #FF8F00 !important; }
|
.btn-singleview:hover { background: #E0E0E0 !important; color: #FF8F00 !important; }
|
||||||
.btn-spread-on, .content-actions .btn-pin-on { background: #FF6F00 !important; color: #FFFFFF !important; }
|
.btn-spread-on, .content-actions .btn-pin-on { background: #FF6F00 !important; color: #FFFFFF !important; }
|
||||||
.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; }
|
||||||
.content-actions .btn-singleview.btn-content { border: 1px solid #FF6F00 !important; }
|
.btn-singleview.btn-content { background: #FF6F00 !important; color: #F0F0F0 !important; border: 1px solid #FF6F00 !important; }
|
||||||
.content-actions .btn-singleview.btn-content:hover { background: rgba(255, 111, 0, .18) !important; border-color: #FF6F00 !important; }
|
.btn-singleview.btn-content:hover { background: #FF8F00 !important; color: #FFFFFF !important; border-color: #FF8F00 !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; }
|
||||||
|
|
@ -344,6 +344,7 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.tribe-members-count { color: #FF6F00 !important; }
|
.tribe-members-count { color: #FF6F00 !important; }
|
||||||
.tribe-card-actions { border-color: #E0E0E0 !important; background: #F8F8F8 !important; }
|
.tribe-card-actions { border-color: #E0E0E0 !important; background: #F8F8F8 !important; }
|
||||||
.tribe-action-btn { border-color: #FF6F00 !important; color: #FF6F00 !important; }
|
.tribe-action-btn { border-color: #FF6F00 !important; color: #FF6F00 !important; }
|
||||||
|
button.tribe-action-btn { background: transparent !important; border: 1px solid #FF6F00 !important; }
|
||||||
.tribe-action-btn:hover { background: #FF6F00 !important; color: #fff !important; }
|
.tribe-action-btn:hover { background: #FF6F00 !important; color: #fff !important; }
|
||||||
.tribe-subtribe-link { background: #F0F0F0 !important; border-color: #E0E0E0 !important; color: #FF6F00 !important; }
|
.tribe-subtribe-link { background: #F0F0F0 !important; border-color: #E0E0E0 !important; color: #FF6F00 !important; }
|
||||||
.tribe-thumb-link { border-color: #E0E0E0 !important; }
|
.tribe-thumb-link { border-color: #E0E0E0 !important; }
|
||||||
|
|
|
||||||
|
|
@ -318,8 +318,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-singleview:hover { background: #2d2e34 !important; color: #FFD700 !important; }
|
.btn-singleview:hover { background: #2d2e34 !important; color: #FFD700 !important; }
|
||||||
.btn-spread-on, .content-actions .btn-pin-on { background: #ffa300 !important; color: #000000 !important; }
|
.btn-spread-on, .content-actions .btn-pin-on { background: #ffa300 !important; color: #000000 !important; }
|
||||||
.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; }
|
||||||
.content-actions .btn-singleview.btn-content { border: 1px solid #ffa300 !important; }
|
.btn-singleview.btn-content { background: #ffa300 !important; color: #1e1f23 !important; border: 1px solid #ffa300 !important; }
|
||||||
.content-actions .btn-singleview.btn-content:hover { background: rgba(255, 163, 0, .18) !important; border-color: #ffa300 !important; }
|
.btn-singleview.btn-content:hover { background: #FFD700 !important; color: #1e1f23 !important; border-color: #FFD700 !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; }
|
||||||
|
|
|
||||||
|
|
@ -320,8 +320,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-singleview:hover { background: #00FF00 !important; color: #000000 !important; }
|
.btn-singleview:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||||
.btn-spread-on, .content-actions .btn-pin-on { background: #00FF00 !important; color: #000000 !important; }
|
.btn-spread-on, .content-actions .btn-pin-on { background: #00FF00 !important; color: #000000 !important; }
|
||||||
.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; }
|
||||||
.content-actions .btn-singleview.btn-content { border: 1px solid #00FF00 !important; }
|
.btn-singleview.btn-content { background: #00FF00 !important; color: #000000 !important; border: 1px solid #00FF00 !important; }
|
||||||
.content-actions .btn-singleview.btn-content:hover { background: rgba(0, 255, 0, .18) !important; border-color: #00FF00 !important; }
|
.btn-singleview.btn-content:hover { background: #000000 !important; color: #00FF00 !important; border-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; }
|
||||||
|
|
|
||||||
|
|
@ -1064,3 +1064,116 @@ body:has(.oasis-nav-side-toggle) { padding-top: 84px !important; }
|
||||||
.oasis-nav-side-toggle ~ .header .top-bar-left nav,
|
.oasis-nav-side-toggle ~ .header .top-bar-left nav,
|
||||||
.oasis-nav-side-toggle ~ .header .top-bar-right nav,
|
.oasis-nav-side-toggle ~ .header .top-bar-right nav,
|
||||||
.oasis-nav-side-toggle ~ .header .top-bar-left a.logo-icon { display: none !important; }
|
.oasis-nav-side-toggle ~ .header .top-bar-left a.logo-icon { display: none !important; }
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Upstream 1.3.3: piezas nuevas (karma del perfil, directo a pantalla completa,
|
||||||
|
salas en vivo, graphos). Copiado tal cual del tema oficial: el resto de ese
|
||||||
|
tema es otro aspecto y no se trae.
|
||||||
|
========================================================================== */
|
||||||
|
.main-column .live-lightbox.live-fullscreen {
|
||||||
|
top: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
z-index: 10050 !important;
|
||||||
|
background: #000 !important;
|
||||||
|
}
|
||||||
|
.main-column .live-lightbox .live-fullscreen-btn {
|
||||||
|
top: 8px !important;
|
||||||
|
right: 64px !important;
|
||||||
|
z-index: 2 !important;
|
||||||
|
color: #FFD700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-sensors-box span.karma-line.karma-line-stacked {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 1px !important;
|
||||||
|
}
|
||||||
|
.profile-sensors-box span.karma-line .karma-line-main {
|
||||||
|
display: block !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
|
.profile-sensors-box span.karma-line .karma-received-row {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 14px !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
|
.profile-sensors-box span.karma-line .karma-received {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-column .room-live-panel .room-live-head {
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-head .room-live-controls {
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-controls .tribe-action-btn {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 5px 10px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-controls .room-stream-input:invalid ~ .room-stream-go,
|
||||||
|
.main-column .room-live-controls .room-stream-input:valid ~ .room-stream-pick {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.main-column .room-stream-chip {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 2px 10px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
background: rgba(255, 163, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-panel .video-grid {
|
||||||
|
margin: 10px 0 !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-panel #room-video {
|
||||||
|
scroll-margin-top: 80px !important;
|
||||||
|
}
|
||||||
|
.main-column .room-live-panel .phone-contacts {
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
.main-column .phone-contact.room-peer {
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 4px 6px !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
.main-column .phone-contact.room-peer .user-link {
|
||||||
|
padding: 4px 10px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
.main-column .phone-contact.room-peer :is(.room-peer-you, .room-peer-cam, .room-peer-muted, .room-peer-hand) {
|
||||||
|
padding: 1px 5px !important;
|
||||||
|
font-size: 10px !important;
|
||||||
|
}
|
||||||
|
.main-column .phone-contact.room-peer :is(button, .tribe-action-btn) {
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 3px 8px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
.main-column .live-lightbox.live-fullscreen {
|
||||||
|
top: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
.main-column .video-grid-solo .video-tile { width: 100% !important; }
|
||||||
|
.main-column .inhabitants-grid .inhabitant-card { padding-bottom: 12px !important; }
|
||||||
|
|
|
||||||
|
|
@ -355,8 +355,8 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.btn-singleview:hover { background: #5A1A85 !important; color: #FFD600 !important; }
|
.btn-singleview:hover { background: #5A1A85 !important; color: #FFD600 !important; }
|
||||||
.btn-spread-on, .content-actions .btn-pin-on { background: #B86ADE !important; color: #2A0E42 !important; }
|
.btn-spread-on, .content-actions .btn-pin-on { background: #B86ADE !important; color: #2A0E42 !important; }
|
||||||
.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; }
|
||||||
.content-actions .btn-singleview.btn-content { border: 1px solid #B86ADE !important; }
|
.btn-singleview.btn-content { background: #B86ADE !important; color: #4B1A72 !important; border: 1px solid #B86ADE !important; }
|
||||||
.content-actions .btn-singleview.btn-content:hover { background: rgba(184, 106, 222, .18) !important; border-color: #B86ADE !important; }
|
.btn-singleview.btn-content:hover { background: #FFD600 !important; color: #4B1A72 !important; border-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; }
|
||||||
|
|
@ -381,6 +381,7 @@ a.user-link:focus, button.user-link:focus {
|
||||||
.tribe-members-count { color: #FFD600 !important; }
|
.tribe-members-count { color: #FFD600 !important; }
|
||||||
.tribe-card-actions { border-color: #B86ADE !important; background: #2D0B47 !important; }
|
.tribe-card-actions { border-color: #B86ADE !important; background: #2D0B47 !important; }
|
||||||
.tribe-action-btn { border-color: #B86ADE !important; color: #B86ADE !important; }
|
.tribe-action-btn { border-color: #B86ADE !important; color: #B86ADE !important; }
|
||||||
|
button.tribe-action-btn { background: transparent !important; }
|
||||||
.tribe-action-btn:hover { background: #B86ADE !important; color: #000 !important; }
|
.tribe-action-btn:hover { background: #B86ADE !important; color: #000 !important; }
|
||||||
.tribe-subtribe-link { background: #4B1A72 !important; border-color: #B86ADE !important; color: #FFD600 !important; }
|
.tribe-subtribe-link { background: #4B1A72 !important; border-color: #B86ADE !important; color: #FFD600 !important; }
|
||||||
.tribe-thumb-link { border-color: #B86ADE !important; }
|
.tribe-thumb-link { border-color: #B86ADE !important; }
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "يمكن الوصول إليه عبر Tor",
|
peersTorYes: "يمكن الوصول إليه عبر Tor",
|
||||||
peersTorNo: "لا يمكن الوصول إليه عبر Tor",
|
peersTorNo: "لا يمكن الوصول إليه عبر Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "الإصدار",
|
peersOasisVersion: "الإصدار",
|
||||||
graphos: "غرافوس",
|
graphos: "غرافوس",
|
||||||
graphosDescription: "خريطة تفاعلية للشبكة من حولك.",
|
graphosDescription: "خريطة تفاعلية للشبكة من حولك.",
|
||||||
|
|
@ -602,6 +602,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "لا يعلن عن إصداره: Oasis قديم",
|
peerVersionUnannounced: "لا يعلن عن إصداره: Oasis قديم",
|
||||||
peersTorMissing: "هذا الـPUB لا يسمح إلا بالاتصالات عبر TOR.",
|
peersTorMissing: "هذا الـPUB لا يسمح إلا بالاتصالات عبر TOR.",
|
||||||
peerConnectionsTitle: "الاتصالات",
|
peerConnectionsTitle: "الاتصالات",
|
||||||
|
peersOnlineHint: "العقد المتصلة بك الآن.",
|
||||||
|
peersDiscoveredHint: "عقد ذات عنوان معروف غير متصلة حاليًا.",
|
||||||
|
peersUnknownHint: "عقد في شبكتك الاجتماعية بدون عنوان معروف.",
|
||||||
|
peersConnectionsHint: "العناوين التي يمكن لهذه العقدة الاتصال بها وحالة كل اتصال.",
|
||||||
|
peersTabLan: "الشبكة المحلية",
|
||||||
|
peersTabHealth: "الحالة",
|
||||||
|
peersHealthHint: "حال كل pub أو نظير معروف، داخل الشبكة المحلية وخارجها.",
|
||||||
|
peersHealthReachable: "يمكن الوصول إليه",
|
||||||
|
peersHealthFailing: "يفشل",
|
||||||
|
peersHealthFailures: "الإخفاقات",
|
||||||
|
peersFailingSince: "يفشل منذ",
|
||||||
|
peersHealthForget: "نسيان غير القابلة للوصول",
|
||||||
|
peersLanHint: "العقد التي تم العثور عليها في شبكتك المحلية.",
|
||||||
|
peersLanLastSeen: "آخر ظهور",
|
||||||
|
peersLanEmpty: "لم يُعثر على أي عقدة في الشبكة المحلية بعد.",
|
||||||
online: "متصل",
|
online: "متصل",
|
||||||
offline: "غير متصل",
|
offline: "غير متصل",
|
||||||
discovered: 'مُكتشَف',
|
discovered: 'مُكتشَف',
|
||||||
|
|
@ -1650,8 +1665,6 @@ module.exports = {
|
||||||
dataTermsTitle: "الكلمات المفتاحية",
|
dataTermsTitle: "الكلمات المفتاحية",
|
||||||
dataTermsHint: "المصطلحات التي تظهر في أكثر المحتويات.",
|
dataTermsHint: "المصطلحات التي تظهر في أكثر المحتويات.",
|
||||||
dataConnections: "تطابقات مرتبطة",
|
dataConnections: "تطابقات مرتبطة",
|
||||||
dataTopicTitle: "كل ما يتعلق بـ",
|
|
||||||
dataTopicHint: "ما يربطه الخوارزم بهذا الموضوع",
|
|
||||||
dataNoMatches: "لا توجد بيانات متقاطعة لعرضها.",
|
dataNoMatches: "لا توجد بيانات متقاطعة لعرضها.",
|
||||||
dataNoProfile: "انشر محتوى أو اكتب سيرتك الذاتية ليتعلم الخوارزم ما يهمك.",
|
dataNoProfile: "انشر محتوى أو اكتب سيرتك الذاتية ليتعلم الخوارزم ما يهمك.",
|
||||||
pollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.",
|
pollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.",
|
||||||
|
|
@ -2204,9 +2217,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "التفاعل",
|
tribeTrendingEngagement: "التفاعل",
|
||||||
tribeTopCategory: "الأكثر تصويتًا",
|
tribeTopCategory: "الأكثر تصويتًا",
|
||||||
tribeTagsEmpty: "لم يتم العثور على وسوم.",
|
tribeTagsEmpty: "لم يتم العثور على وسوم.",
|
||||||
tribeSearchPlaceholder: "ابحث في محتوى القبيلة...",
|
|
||||||
tribeSearchEmpty: "لم يتم العثور على نتائج.",
|
tribeSearchEmpty: "لم يتم العثور على نتائج.",
|
||||||
tribeSearchResults: "النتائج",
|
|
||||||
tribeSearchMinChars: "أدخل حرفين على الأقل للبحث.",
|
tribeSearchMinChars: "أدخل حرفين على الأقل للبحث.",
|
||||||
agendaTitle: "الأجندة",
|
agendaTitle: "الأجندة",
|
||||||
agendaDescription: "هنا يمكنك العثور على جميع العناصر المسندة إليك.",
|
agendaDescription: "هنا يمكنك العثور على جميع العناصر المسندة إليك.",
|
||||||
|
|
@ -3835,6 +3846,7 @@ module.exports = {
|
||||||
roomUpdate: "تحديث",
|
roomUpdate: "تحديث",
|
||||||
roomDelete: "حذف",
|
roomDelete: "حذف",
|
||||||
roomClose: "إغلاق",
|
roomClose: "إغلاق",
|
||||||
|
roomReopen: "إعادة فتح",
|
||||||
roomCreateSectionTitle: "إنشاء غرفة",
|
roomCreateSectionTitle: "إنشاء غرفة",
|
||||||
roomUpdateSectionTitle: "تحديث الغرفة",
|
roomUpdateSectionTitle: "تحديث الغرفة",
|
||||||
roomTitlePlaceholder: "عنوان الغرفة",
|
roomTitlePlaceholder: "عنوان الغرفة",
|
||||||
|
|
@ -3853,6 +3865,15 @@ module.exports = {
|
||||||
roomMuted: "مكتوم",
|
roomMuted: "مكتوم",
|
||||||
roomRec: "تسجيل الصوت",
|
roomRec: "تسجيل الصوت",
|
||||||
roomRecStop: "إيقاف التسجيل",
|
roomRecStop: "إيقاف التسجيل",
|
||||||
|
roomStreamFile: "بثّ",
|
||||||
|
roomStreamStop: "إيقاف",
|
||||||
|
roomStreaming: "جارٍ البث",
|
||||||
|
liveFullscreen: "ملء الشاشة",
|
||||||
|
roomStreamPickTitle: "اختر فيديو",
|
||||||
|
roomStreamPickHint: "مقاطع الفيديو على هذا الجهاز. يُبث الفيديو الذي تختاره من مكانه دون نسخه أو نشره.",
|
||||||
|
roomStreamGo: "بثّه في الغرفة",
|
||||||
|
roomStreamNotFound: "هذا الفيديو ليس في مجلداتك.",
|
||||||
|
roomPublicWarning: "تُبث هذه الغرفة على الويب العام: يمكن لأي شخص رؤية وسماع ما يحدث فيها.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4333,7 +4354,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "السجل",
|
phoneHistoryTitle: "السجل",
|
||||||
phoneLatestTitle: "آخر المكالمات",
|
phoneLatestTitle: "آخر المكالمات",
|
||||||
phoneCreateButton: "مكالمة جديدة",
|
phoneCreateButton: "مكالمة جديدة",
|
||||||
phoneLimitsHint: "حتى 7 من السكان لكل مكالمة؛ أكثر من ساكن واحد يجعلها مكالمة مشتركة.",
|
phoneLimitsHint: "حتى {n} من السكان لكل مكالمة؛ أكثر من ساكن واحد يجعلها مكالمة مشتركة.",
|
||||||
phoneErrorPub: "لا يمكن الاتصال بخادم pub.",
|
phoneErrorPub: "لا يمكن الاتصال بخادم pub.",
|
||||||
phoneJointCall: "مكالمة مشتركة",
|
phoneJointCall: "مكالمة مشتركة",
|
||||||
phoneWith: "مع",
|
phoneWith: "مع",
|
||||||
|
|
@ -4478,6 +4499,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "قم بتكوين مستوى التعرض للرسائل الخاصة في الشبكة.",
|
settingsPmVisibilityDesc: "قم بتكوين مستوى التعرض للرسائل الخاصة في الشبكة.",
|
||||||
settingsPmVisibilityWhole: "الكون المتعدد",
|
settingsPmVisibilityWhole: "الكون المتعدد",
|
||||||
settingsPmVisibilityMutuals: "الدعم المتبادل فقط",
|
settingsPmVisibilityMutuals: "الدعم المتبادل فقط",
|
||||||
|
mute: "كتم",
|
||||||
|
unmute: "إلغاء الكتم",
|
||||||
|
relationshipMuted: "مكتوم من قبلك",
|
||||||
|
hideContent: "إخفاء عني",
|
||||||
|
unhideContent: "إظهار مجددًا",
|
||||||
|
hiddenByYou: "مخفي من قبلك",
|
||||||
|
settingsTabContent: "المحتوى",
|
||||||
|
settingsHiddenTitle: "المحتوى المخفي",
|
||||||
|
settingsHiddenDesc: "السكان الذين كتمتهم والمحتوى الذي أخفيته.",
|
||||||
|
settingsMutedLabel: "السكان المكتومون",
|
||||||
|
settingsHiddenLabel: "المحتوى المخفي",
|
||||||
|
settingsMutedEmpty: "لا أحد مكتوم.",
|
||||||
|
settingsHiddenEmpty: "لا شيء مخفي.",
|
||||||
|
settingsPmVisibilityFollowing: "فقط من أتابعهم",
|
||||||
|
settingsMentionsVisibilityTitle: "الإشارات",
|
||||||
|
settingsMentionsVisibilityDesc: "اختر من تصلك إشاراته.",
|
||||||
pmMutualNotice: "الإرسال حاجز UX. الاستقبال فلتر انتباه.",
|
pmMutualNotice: "الإرسال حاجز UX. الاستقبال فلتر انتباه.",
|
||||||
inhabitantsPendingFollowsTitle: "طلبات الدعم المعلقة",
|
inhabitantsPendingFollowsTitle: "طلبات الدعم المعلقة",
|
||||||
inhabitantsPendingAccept: "قبول",
|
inhabitantsPendingAccept: "قبول",
|
||||||
|
|
@ -4610,7 +4647,7 @@ module.exports = {
|
||||||
bankRulesFloor: "الحد الأدنى لكل مستخدم (إجمالي)",
|
bankRulesFloor: "الحد الأدنى لكل مستخدم (إجمالي)",
|
||||||
bankRulesGraceDays: "فترة السماح للمطالبة",
|
bankRulesGraceDays: "فترة السماح للمطالبة",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "تُشتق الكارما من أفعالك (المنشورات، والأصوات، والمشاركة في البرلمان/المحاكم، وغير ذلك) مع معامل تناقص بمرور الوقت، مطروحًا منها غرامات الكربون التي تولّدها تغذيتك.",
|
bankRulesKarmaNote: "تُشتق الكارما من أفعالك (المنشورات، والأصوات، والمشاركة في البرلمان/المحاكم، وغير ذلك) مع معامل تناقص بمرور الوقت، مطروحًا منها غرامات الكربون التي تولّدها تغذيتك. ما يتلقاه محتواك يُحتسب أيضًا: الآراء والأصوات والتفاعلات والتشغيلات الإيجابية تضيف كارما، والآراء أو الأصوات السلبية تخصمها عندما يتفق عدة سكان.",
|
||||||
bankRulesKarmaTitle: "الكارما (درجة تفاعل المستخدم)",
|
bankRulesKarmaTitle: "الكارما (درجة تفاعل المستخدم)",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
|
|
@ -4696,7 +4733,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "في اختبار WILL يمنح كل خيار وزنًا لبيت أو أكثر؛ وعند الإرسال يجمع النظام النقاط ويضمّك تلقائيًا إلى البيت الأعلى نقاطًا.",
|
larpRulesTestEntryText: "في اختبار WILL يمنح كل خيار وزنًا لبيت أو أكثر؛ وعند الإرسال يجمع النظام النقاط ويضمّك تلقائيًا إلى البيت الأعلى نقاطًا.",
|
||||||
larpWillTestHint: "بمجرد الانتهاء، ستُسند إليك البيت الذي يتطابق بشكل أفضل مع إجاباتك. تتم معالجة إجاباتك الفردية محلياً ولا يتم تخزينها أبداً — يُنشر في خلاصتك فقط نتيجة تعيين البيت.",
|
larpWillTestHint: "بمجرد الانتهاء، ستُسند إليك البيت الذي يتطابق بشكل أفضل مع إجاباتك. تتم معالجة إجاباتك الفردية محلياً ولا يتم تخزينها أبداً — يُنشر في خلاصتك فقط نتيجة تعيين البيت.",
|
||||||
larpWillTestTitle: "اختبار WILL",
|
larpWillTestTitle: "اختبار WILL",
|
||||||
settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية. تعطيل لإيقاف بث UDP.",
|
settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية.",
|
||||||
settingsLanEnable: "تفعيل بث الشبكة المحلية",
|
settingsLanEnable: "تفعيل بث الشبكة المحلية",
|
||||||
settingsLanTitle: "بث الشبكة المحلية",
|
settingsLanTitle: "بث الشبكة المحلية",
|
||||||
settingsReplicationTitle: "النسخ",
|
settingsReplicationTitle: "النسخ",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Accessible per Tor",
|
peersTorYes: "Accessible per Tor",
|
||||||
peersTorNo: "No accessible per Tor",
|
peersTorNo: "No accessible per Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Versió",
|
peersOasisVersion: "Versió",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Mapa interactiu de la xarxa al teu voltant.",
|
graphosDescription: "Mapa interactiu de la xarxa al teu voltant.",
|
||||||
|
|
@ -594,6 +594,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "No anuncia la seva versió: un Oasis antic",
|
peerVersionUnannounced: "No anuncia la seva versió: un Oasis antic",
|
||||||
peersTorMissing: "Aquest PUB només permet connexions des de TOR.",
|
peersTorMissing: "Aquest PUB només permet connexions des de TOR.",
|
||||||
peerConnectionsTitle: "Connexions",
|
peerConnectionsTitle: "Connexions",
|
||||||
|
peersOnlineHint: "Nodes connectats amb tu ara mateix.",
|
||||||
|
peersDiscoveredHint: "Nodes amb adreça coneguda que no estan connectats ara.",
|
||||||
|
peersUnknownHint: "Nodes del teu graf social sense adreça coneguda.",
|
||||||
|
peersConnectionsHint: "Adreces a les quals pot connectar aquest node i l'estat de cada connexió.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Salut",
|
||||||
|
peersHealthHint: "Com està cada pub o node conegut, a la xarxa local i fora.",
|
||||||
|
peersHealthReachable: "Accessible",
|
||||||
|
peersHealthFailing: "Fallant",
|
||||||
|
peersHealthFailures: "Errors",
|
||||||
|
peersFailingSince: "Falla des de",
|
||||||
|
peersHealthForget: "Oblidar inaccessibles",
|
||||||
|
peersLanHint: "Nodes trobats a la teva xarxa local.",
|
||||||
|
peersLanLastSeen: "Vist per última vegada",
|
||||||
|
peersLanEmpty: "Encara no s'ha trobat cap node a la xarxa local.",
|
||||||
online: "Connectat",
|
online: "Connectat",
|
||||||
offline: "Desconnectat",
|
offline: "Desconnectat",
|
||||||
discovered: "Descoberts",
|
discovered: "Descoberts",
|
||||||
|
|
@ -1639,8 +1654,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Paraules clau",
|
dataTermsTitle: "Paraules clau",
|
||||||
dataTermsHint: "Els termes que apareixen en més continguts.",
|
dataTermsHint: "Els termes que apareixen en més continguts.",
|
||||||
dataConnections: "Coincidències enllaçades",
|
dataConnections: "Coincidències enllaçades",
|
||||||
dataTopicTitle: "Tot el relacionat amb",
|
|
||||||
dataTopicHint: "El que l'algorisme relaciona amb aquest tema",
|
|
||||||
dataNoMatches: "No hi ha dades creuades per mostrar.",
|
dataNoMatches: "No hi ha dades creuades per mostrar.",
|
||||||
dataNoProfile: "Publica contingut o escriu el teu CV perquè l'algorisme aprengui què t'interessa.",
|
dataNoProfile: "Publica contingut o escriu el teu CV perquè l'algorisme aprengui què t'interessa.",
|
||||||
pollsDescription: "Mòdul per preguntar a la xarxa i comptar les respostes.",
|
pollsDescription: "Mòdul per preguntar a la xarxa i comptar les respostes.",
|
||||||
|
|
@ -2194,9 +2207,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "interacció",
|
tribeTrendingEngagement: "interacció",
|
||||||
tribeTopCategory: "Més votat",
|
tribeTopCategory: "Més votat",
|
||||||
tribeTagsEmpty: "No s'han trobat etiquetes.",
|
tribeTagsEmpty: "No s'han trobat etiquetes.",
|
||||||
tribeSearchPlaceholder: "Cercar contingut de la tribu...",
|
|
||||||
tribeSearchEmpty: "Sense resultats.",
|
tribeSearchEmpty: "Sense resultats.",
|
||||||
tribeSearchResults: "Resultats",
|
|
||||||
tribeSearchMinChars: "Introdueix almenys 2 caràcters per cercar.",
|
tribeSearchMinChars: "Introdueix almenys 2 caràcters per cercar.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Aquí pots trobar tots els teus elements assignats.",
|
agendaDescription: "Aquí pots trobar tots els teus elements assignats.",
|
||||||
|
|
@ -3833,6 +3844,7 @@ module.exports = {
|
||||||
roomUpdate: "Actualitzar",
|
roomUpdate: "Actualitzar",
|
||||||
roomDelete: "Eliminar",
|
roomDelete: "Eliminar",
|
||||||
roomClose: "Tancar",
|
roomClose: "Tancar",
|
||||||
|
roomReopen: "Reobrir",
|
||||||
roomCreateSectionTitle: "Crear sala",
|
roomCreateSectionTitle: "Crear sala",
|
||||||
roomUpdateSectionTitle: "Actualitzar sala",
|
roomUpdateSectionTitle: "Actualitzar sala",
|
||||||
roomTitlePlaceholder: "Títol de la sala",
|
roomTitlePlaceholder: "Títol de la sala",
|
||||||
|
|
@ -3851,6 +3863,15 @@ module.exports = {
|
||||||
roomMuted: "silenciat",
|
roomMuted: "silenciat",
|
||||||
roomRec: "Gravar àudio",
|
roomRec: "Gravar àudio",
|
||||||
roomRecStop: "Atura la gravació",
|
roomRecStop: "Atura la gravació",
|
||||||
|
roomStreamFile: "Emet",
|
||||||
|
roomStreamStop: "Atura",
|
||||||
|
roomStreaming: "Emetent",
|
||||||
|
liveFullscreen: "Pantalla completa",
|
||||||
|
roomStreamPickTitle: "Tria un vídeo",
|
||||||
|
roomStreamPickHint: "Vídeos d'aquest dispositiu. El que triïs s'emet des d'on és, sense copiar-lo ni publicar-lo.",
|
||||||
|
roomStreamGo: "Emet a la sala",
|
||||||
|
roomStreamNotFound: "Aquest vídeo no és a les teves carpetes.",
|
||||||
|
roomPublicWarning: "Aquesta sala es retransmet a la web pública: qualsevol pot veure i sentir el que hi passa.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4332,7 +4353,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Historial",
|
phoneHistoryTitle: "Historial",
|
||||||
phoneLatestTitle: "Últimes trucades",
|
phoneLatestTitle: "Últimes trucades",
|
||||||
phoneCreateButton: "Nova trucada",
|
phoneCreateButton: "Nova trucada",
|
||||||
phoneLimitsHint: "Fins a 7 habitants per trucada; amb més d'un és una trucada conjunta.",
|
phoneLimitsHint: "Fins a {n} habitants per trucada; amb més d'un és una trucada conjunta.",
|
||||||
phoneErrorPub: "No es pot trucar a un pub.",
|
phoneErrorPub: "No es pot trucar a un pub.",
|
||||||
phoneJointCall: "Trucada conjunta",
|
phoneJointCall: "Trucada conjunta",
|
||||||
phoneWith: "Amb",
|
phoneWith: "Amb",
|
||||||
|
|
@ -4477,6 +4498,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configura el nivell d'exposició a missatges privats que vols tenir a la xarxa.",
|
settingsPmVisibilityDesc: "Configura el nivell d'exposició a missatges privats que vols tenir a la xarxa.",
|
||||||
settingsPmVisibilityWhole: "Multivers",
|
settingsPmVisibilityWhole: "Multivers",
|
||||||
settingsPmVisibilityMutuals: "Només suport mutu",
|
settingsPmVisibilityMutuals: "Només suport mutu",
|
||||||
|
mute: "Silenciar",
|
||||||
|
unmute: "Deixar de silenciar",
|
||||||
|
relationshipMuted: "Silenciat per tu",
|
||||||
|
hideContent: "Amagar per a mi",
|
||||||
|
unhideContent: "Mostrar de nou",
|
||||||
|
hiddenByYou: "Amagat per tu",
|
||||||
|
settingsTabContent: "Contingut",
|
||||||
|
settingsHiddenTitle: "Contingut amagat",
|
||||||
|
settingsHiddenDesc: "Habitants que has silenciat i contingut que has amagat.",
|
||||||
|
settingsMutedLabel: "Habitants silenciats",
|
||||||
|
settingsHiddenLabel: "Contingut amagat",
|
||||||
|
settingsMutedEmpty: "Ningú silenciat.",
|
||||||
|
settingsHiddenEmpty: "Res amagat.",
|
||||||
|
settingsPmVisibilityFollowing: "Només gent que segueixo",
|
||||||
|
settingsMentionsVisibilityTitle: "Mencions",
|
||||||
|
settingsMentionsVisibilityDesc: "Tria de qui t'arriben les mencions.",
|
||||||
pmMutualNotice: "L'enviament és una barrera d'UX. La recepció és un filtre d'atenció (el xifratge ja està replicat).",
|
pmMutualNotice: "L'enviament és una barrera d'UX. La recepció és un filtre d'atenció (el xifratge ja està replicat).",
|
||||||
inhabitantsPendingFollowsTitle: "Sol·licituds de suport pendents",
|
inhabitantsPendingFollowsTitle: "Sol·licituds de suport pendents",
|
||||||
inhabitantsPendingAccept: "Acceptar",
|
inhabitantsPendingAccept: "Acceptar",
|
||||||
|
|
@ -4583,7 +4620,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "Taxa ECO(usuari) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "Taxa ECO(usuari) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "L'impost ECO es dedueix de la RBU bruta quan es calcula l'assignació de l'època. Els ECO deduïts no es redistribueixen en aquesta època — es queden al saldo del pub i alimenten el fons de la pròxima època a través dels límits reserveMin / alpha.",
|
bankRulesEcoTaxNote: "L'impost ECO es dedueix de la RBU bruta quan es calcula l'assignació de l'època. Els ECO deduïts no es redistribueixen en aquesta època — es queden al saldo del pub i alimenten el fons de la pròxima època a través dels límits reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
||||||
bankRulesKarmaNote: "El karma es deriva de les teves accions (publicacions, vots, participació al parlament/tribunals, etc.) amb un factor de decaïment temporal, menys els grams de carboni que genera el teu feed.",
|
bankRulesKarmaNote: "El karma es deriva de les teves accions (publicacions, vots, participació al parlament/tribunals, etc.) amb un factor de decaïment temporal, menys els grams de carboni que genera el teu feed. El que rep el teu contingut també compta: les opinions, vots, reaccions i reproduccions positives sumen karma, i les opinions o vots negatius el resten quan diversos habitants coincideixen.",
|
||||||
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
||||||
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
||||||
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
||||||
|
|
@ -4618,7 +4655,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "Al test WILL cada opció pondera una o més cases; en enviar-lo, el sistema suma les puntuacions i t'admet automàticament a la casa amb més punts.",
|
larpRulesTestEntryText: "Al test WILL cada opció pondera una o més cases; en enviar-lo, el sistema suma les puntuacions i t'admet automàticament a la casa amb més punts.",
|
||||||
larpWillTestHint: "Un cop completat, se t'assignarà la casa que més s'ajusti a les teves respostes. Les teves respostes individuals es processen localment i no s'emmagatzemen mai — només es publica al teu feed l'assignació de casa resultant.",
|
larpWillTestHint: "Un cop completat, se t'assignarà la casa que més s'ajusti a les teves respostes. Les teves respostes individuals es processen localment i no s'emmagatzemen mai — només es publica al teu feed l'assignació de casa resultant.",
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periòdicament aquest node a altres instàncies d'Oasis de la mateixa xarxa local. Desactiva-ho per aturar les difusions UDP.",
|
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",
|
||||||
settingsLanTitle: "Difusió LAN",
|
settingsLanTitle: "Difusió LAN",
|
||||||
settingsReplicationTitle: "Replicació",
|
settingsReplicationTitle: "Replicació",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Über Tor erreichbar",
|
peersTorYes: "Über Tor erreichbar",
|
||||||
peersTorNo: "Nicht über Tor erreichbar",
|
peersTorNo: "Nicht über Tor erreichbar",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Version",
|
peersOasisVersion: "Version",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Interaktive Karte des Netzwerks um dich herum.",
|
graphosDescription: "Interaktive Karte des Netzwerks um dich herum.",
|
||||||
|
|
@ -598,6 +598,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Gibt seine Version nicht bekannt: ein altes Oasis",
|
peerVersionUnannounced: "Gibt seine Version nicht bekannt: ein altes Oasis",
|
||||||
peersTorMissing: "Dieser PUB erlaubt nur Verbindungen über TOR.",
|
peersTorMissing: "Dieser PUB erlaubt nur Verbindungen über TOR.",
|
||||||
peerConnectionsTitle: "Verbindungen",
|
peerConnectionsTitle: "Verbindungen",
|
||||||
|
peersOnlineHint: "Knoten, die gerade mit dir verbunden sind.",
|
||||||
|
peersDiscoveredHint: "Knoten mit bekannter Adresse, die gerade nicht verbunden sind.",
|
||||||
|
peersUnknownHint: "Knoten in deinem sozialen Graphen ohne bekannte Adresse.",
|
||||||
|
peersConnectionsHint: "Adressen, die dieser Knoten anwählen kann, und der Zustand jeder Verbindung.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Zustand",
|
||||||
|
peersHealthHint: "Wie es jedem bekannten Pub oder Peer geht, im lokalen Netz und darüber hinaus.",
|
||||||
|
peersHealthReachable: "Erreichbar",
|
||||||
|
peersHealthFailing: "Fehlschlagend",
|
||||||
|
peersHealthFailures: "Fehlschläge",
|
||||||
|
peersFailingSince: "Fehlschlagend seit",
|
||||||
|
peersHealthForget: "Unerreichbare vergessen",
|
||||||
|
peersLanHint: "Knoten, die in deinem lokalen Netz gefunden wurden.",
|
||||||
|
peersLanLastSeen: "Zuletzt gesehen",
|
||||||
|
peersLanEmpty: "Noch kein Knoten im lokalen Netz gefunden.",
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
discovered: 'Entdeckt',
|
discovered: 'Entdeckt',
|
||||||
|
|
@ -1644,8 +1659,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Schlagwörter",
|
dataTermsTitle: "Schlagwörter",
|
||||||
dataTermsHint: "Die Begriffe, die in den meisten Inhalten vorkommen.",
|
dataTermsHint: "Die Begriffe, die in den meisten Inhalten vorkommen.",
|
||||||
dataConnections: "Verknüpfte Treffer",
|
dataConnections: "Verknüpfte Treffer",
|
||||||
dataTopicTitle: "Alles zu",
|
|
||||||
dataTopicHint: "Was der Algorithmus mit diesem Thema verbindet",
|
|
||||||
dataNoMatches: "Keine Querdaten vorhanden.",
|
dataNoMatches: "Keine Querdaten vorhanden.",
|
||||||
dataNoProfile: "Veröffentliche Inhalte oder schreib deinen Lebenslauf, damit der Algorithmus lernt, was dich interessiert.",
|
dataNoProfile: "Veröffentliche Inhalte oder schreib deinen Lebenslauf, damit der Algorithmus lernt, was dich interessiert.",
|
||||||
pollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.",
|
pollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.",
|
||||||
|
|
@ -2201,9 +2214,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "Beteiligung",
|
tribeTrendingEngagement: "Beteiligung",
|
||||||
tribeTopCategory: "Meistgewählt",
|
tribeTopCategory: "Meistgewählt",
|
||||||
tribeTagsEmpty: "Keine Tags gefunden.",
|
tribeTagsEmpty: "Keine Tags gefunden.",
|
||||||
tribeSearchPlaceholder: "Stamminhalt durchsuchen...",
|
|
||||||
tribeSearchEmpty: "Keine Ergebnisse gefunden.",
|
tribeSearchEmpty: "Keine Ergebnisse gefunden.",
|
||||||
tribeSearchResults: "Ergebnisse",
|
|
||||||
tribeSearchMinChars: "Mindestens 2 Zeichen eingeben, um zu suchen.",
|
tribeSearchMinChars: "Mindestens 2 Zeichen eingeben, um zu suchen.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Hier findest du alle dir zugewiesenen Einträge.",
|
agendaDescription: "Hier findest du alle dir zugewiesenen Einträge.",
|
||||||
|
|
@ -3833,6 +3844,7 @@ module.exports = {
|
||||||
roomUpdate: "Aktualisieren",
|
roomUpdate: "Aktualisieren",
|
||||||
roomDelete: "Löschen",
|
roomDelete: "Löschen",
|
||||||
roomClose: "Schließen",
|
roomClose: "Schließen",
|
||||||
|
roomReopen: "Wieder öffnen",
|
||||||
roomCreateSectionTitle: "Raum erstellen",
|
roomCreateSectionTitle: "Raum erstellen",
|
||||||
roomUpdateSectionTitle: "Raum aktualisieren",
|
roomUpdateSectionTitle: "Raum aktualisieren",
|
||||||
roomTitlePlaceholder: "Titel des Raums",
|
roomTitlePlaceholder: "Titel des Raums",
|
||||||
|
|
@ -3851,6 +3863,15 @@ module.exports = {
|
||||||
roomMuted: "stumm",
|
roomMuted: "stumm",
|
||||||
roomRec: "Audio aufnehmen",
|
roomRec: "Audio aufnehmen",
|
||||||
roomRecStop: "Aufnahme beenden",
|
roomRecStop: "Aufnahme beenden",
|
||||||
|
roomStreamFile: "Streamen",
|
||||||
|
roomStreamStop: "Stoppen",
|
||||||
|
roomStreaming: "Wird gestreamt",
|
||||||
|
liveFullscreen: "Vollbild",
|
||||||
|
roomStreamPickTitle: "Video auswählen",
|
||||||
|
roomStreamPickHint: "Videos auf diesem Gerät. Das gewählte wird von seinem Speicherort aus gestreamt, ohne es zu kopieren oder zu veröffentlichen.",
|
||||||
|
roomStreamGo: "Im Raum abspielen",
|
||||||
|
roomStreamNotFound: "Dieses Video ist nicht in deinen Ordnern.",
|
||||||
|
roomPublicWarning: "Dieser Raum wird im öffentlichen Web übertragen: Jeder kann sehen und hören, was darin passiert.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4328,7 +4349,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Verlauf",
|
phoneHistoryTitle: "Verlauf",
|
||||||
phoneLatestTitle: "Letzte Anrufe",
|
phoneLatestTitle: "Letzte Anrufe",
|
||||||
phoneCreateButton: "Neuer Anruf",
|
phoneCreateButton: "Neuer Anruf",
|
||||||
phoneLimitsHint: "Bis zu 7 Bewohner pro Anruf; mit mehr als einem ist es ein gemeinsamer Anruf.",
|
phoneLimitsHint: "Bis zu {n} Bewohner pro Anruf; mit mehr als einem ist es ein gemeinsamer Anruf.",
|
||||||
phoneErrorPub: "Pubs können nicht angerufen werden.",
|
phoneErrorPub: "Pubs können nicht angerufen werden.",
|
||||||
phoneJointCall: "Gemeinsamer Anruf",
|
phoneJointCall: "Gemeinsamer Anruf",
|
||||||
phoneWith: "Mit",
|
phoneWith: "Mit",
|
||||||
|
|
@ -4473,6 +4494,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Konfiguriere dein Niveau der Offenlegung für private Nachrichten.",
|
settingsPmVisibilityDesc: "Konfiguriere dein Niveau der Offenlegung für private Nachrichten.",
|
||||||
settingsPmVisibilityWhole: "Multiversum",
|
settingsPmVisibilityWhole: "Multiversum",
|
||||||
settingsPmVisibilityMutuals: "Nur gegenseitige Unterstützung",
|
settingsPmVisibilityMutuals: "Nur gegenseitige Unterstützung",
|
||||||
|
mute: "Stummschalten",
|
||||||
|
unmute: "Stummschaltung aufheben",
|
||||||
|
relationshipMuted: "Von dir stummgeschaltet",
|
||||||
|
hideContent: "Für mich ausblenden",
|
||||||
|
unhideContent: "Wieder anzeigen",
|
||||||
|
hiddenByYou: "Von dir ausgeblendet",
|
||||||
|
settingsTabContent: "Inhalt",
|
||||||
|
settingsHiddenTitle: "Ausgeblendete Inhalte",
|
||||||
|
settingsHiddenDesc: "Stummgeschaltete Bewohner und ausgeblendete Inhalte.",
|
||||||
|
settingsMutedLabel: "Stummgeschaltete Bewohner",
|
||||||
|
settingsHiddenLabel: "Ausgeblendete Inhalte",
|
||||||
|
settingsMutedEmpty: "Niemand stummgeschaltet.",
|
||||||
|
settingsHiddenEmpty: "Nichts ausgeblendet.",
|
||||||
|
settingsPmVisibilityFollowing: "Nur Leute, denen ich folge",
|
||||||
|
settingsMentionsVisibilityTitle: "Erwähnungen",
|
||||||
|
settingsMentionsVisibilityDesc: "Wähle, wessen Erwähnungen dich erreichen.",
|
||||||
pmMutualNotice: "Ausgang ist ein UX-Schutz. Eingang ist ein Aufmerksamkeitsfilter.",
|
pmMutualNotice: "Ausgang ist ein UX-Schutz. Eingang ist ein Aufmerksamkeitsfilter.",
|
||||||
inhabitantsPendingFollowsTitle: "Ausstehende Unterstützungsanfragen",
|
inhabitantsPendingFollowsTitle: "Ausstehende Unterstützungsanfragen",
|
||||||
inhabitantsPendingAccept: "Akzeptieren",
|
inhabitantsPendingAccept: "Akzeptieren",
|
||||||
|
|
@ -4579,7 +4616,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "Die ECO-Steuer wird vom Brutto-UBI abgezogen, wenn die Zuteilung der Epoche berechnet wird. Die abgezogenen ECO werden in dieser Epoche nicht umverteilt — sie bleiben im Pub-Guthaben und speisen den Pool der nächsten Epoche über die Obergrenzen reserveMin / alpha.",
|
bankRulesEcoTaxNote: "Die ECO-Steuer wird vom Brutto-UBI abgezogen, wenn die Zuteilung der Epoche berechnet wird. Die abgezogenen ECO werden in dieser Epoche nicht umverteilt — sie bleiben im Pub-Guthaben und speisen den Pool der nächsten Epoche über die Obergrenzen reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "Karma ergibt sich aus deinen Aktionen (Beiträge, Stimmen, Beteiligung an Parlament/Gerichten usw.) mit einem zeitlichen Abklingfaktor, abzüglich der von deinem Feed erzeugten Gramm CO2.",
|
bankRulesKarmaNote: "Karma ergibt sich aus deinen Aktionen (Beiträge, Stimmen, Beteiligung an Parlament/Gerichten usw.) mit einem zeitlichen Abklingfaktor, abzüglich der von deinem Feed erzeugten Gramm CO2. Was deine Inhalte erhalten, zählt ebenfalls: positive Meinungen, Stimmen, Reaktionen und Wiedergaben geben Karma, negative Meinungen oder Stimmen ziehen es ab, sobald mehrere Bewohner übereinstimmen.",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
||||||
|
|
@ -4613,7 +4650,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "Im WILL-Test gewichtet jede Option ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch im Haus mit der höchsten Punktzahl auf.",
|
larpRulesTestEntryText: "Im WILL-Test gewichtet jede Option ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch im Haus mit der höchsten Punktzahl auf.",
|
||||||
larpWillTestHint: "Nach Abschluss wirst du dem Haus zugewiesen, das am besten zu deinen Antworten passt. Deine einzelnen Antworten werden lokal verarbeitet und nie gespeichert — nur die resultierende Hauszuweisung wird in deinem Feed veröffentlicht.",
|
larpWillTestHint: "Nach Abschluss wirst du dem Haus zugewiesen, das am besten zu deinen Antworten passt. Deine einzelnen Antworten werden lokal verarbeitet und nie gespeichert — nur die resultierende Hauszuweisung wird in deinem Feed veröffentlicht.",
|
||||||
larpWillTestTitle: "WILL-Test",
|
larpWillTestTitle: "WILL-Test",
|
||||||
settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen. Deaktivieren, um UDP-Broadcasts zu stoppen.",
|
settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen.",
|
||||||
settingsLanEnable: "LAN-Broadcasting aktivieren",
|
settingsLanEnable: "LAN-Broadcasting aktivieren",
|
||||||
settingsLanTitle: "LAN-Broadcasting",
|
settingsLanTitle: "LAN-Broadcasting",
|
||||||
settingsReplicationTitle: "Replikation",
|
settingsReplicationTitle: "Replikation",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Reachable over Tor",
|
peersTorYes: "Reachable over Tor",
|
||||||
peersTorNo: "Not reachable over Tor",
|
peersTorNo: "Not reachable over Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Version",
|
peersOasisVersion: "Version",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Interactive map of the network around you.",
|
graphosDescription: "Interactive map of the network around you.",
|
||||||
|
|
@ -500,7 +500,7 @@ module.exports = {
|
||||||
ssbLogStreamDescription: "Configure the message limit for Blockchain streams.",
|
ssbLogStreamDescription: "Configure the message limit for Blockchain streams.",
|
||||||
logWindowChip: "Last {n} blockchain streams",
|
logWindowChip: "Last {n} blockchain streams",
|
||||||
settingsLanTitle: "LAN Broadcasting",
|
settingsLanTitle: "LAN Broadcasting",
|
||||||
settingsLanDesc: "Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.",
|
settingsLanDesc: "Periodically announce this peer to other Oasis instances on the same local network.",
|
||||||
settingsReplicationTitle: "Replication",
|
settingsReplicationTitle: "Replication",
|
||||||
settingsReplicationDesc: "Configure the number of hops your peer follows out from your own feed when replicating content.",
|
settingsReplicationDesc: "Configure the number of hops your peer follows out from your own feed when replicating content.",
|
||||||
settingsReplicationHopsLabel: "Hops",
|
settingsReplicationHopsLabel: "Hops",
|
||||||
|
|
@ -626,6 +626,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "It does not announce its version: an old Oasis",
|
peerVersionUnannounced: "It does not announce its version: an old Oasis",
|
||||||
peersTorMissing: "This PUB only allows connections through TOR.",
|
peersTorMissing: "This PUB only allows connections through TOR.",
|
||||||
peerConnectionsTitle: "Connections",
|
peerConnectionsTitle: "Connections",
|
||||||
|
peersOnlineHint: "Nodes connected to you right now.",
|
||||||
|
peersDiscoveredHint: "Nodes with a known address that are not connected right now.",
|
||||||
|
peersUnknownHint: "Nodes in your social graph with no known address.",
|
||||||
|
peersConnectionsHint: "Addresses this node can dial and the state of each connection.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Health",
|
||||||
|
peersHealthHint: "How each known pub or peer is doing, on the local network and beyond.",
|
||||||
|
peersHealthReachable: "Reachable",
|
||||||
|
peersHealthFailing: "Failing",
|
||||||
|
peersHealthFailures: "Failures",
|
||||||
|
peersFailingSince: "Failing since",
|
||||||
|
peersHealthForget: "Forget unreachable",
|
||||||
|
peersLanHint: "Nodes met on your local network.",
|
||||||
|
peersLanLastSeen: "Last seen",
|
||||||
|
peersLanEmpty: "No nodes met on the local network yet.",
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
discovered: 'Discovered',
|
discovered: 'Discovered',
|
||||||
|
|
@ -1134,7 +1149,7 @@ module.exports = {
|
||||||
bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)",
|
bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)",
|
||||||
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
||||||
bankRulesKarmaTitle: "Karma (user engagement score)",
|
bankRulesKarmaTitle: "Karma (user engagement score)",
|
||||||
bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.",
|
bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed. What your content receives counts too: positive opinions, votes, reactions and plays add karma, and negative opinions or votes subtract it once several inhabitants agree.",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesEcoTaxTitle: "ECO Tax",
|
bankRulesEcoTaxTitle: "ECO Tax",
|
||||||
bankRulesEcoTaxRate: "Rate",
|
bankRulesEcoTaxRate: "Rate",
|
||||||
|
|
@ -1765,8 +1780,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Keywords",
|
dataTermsTitle: "Keywords",
|
||||||
dataTermsHint: "The terms that appear across the most content.",
|
dataTermsHint: "The terms that appear across the most content.",
|
||||||
dataConnections: "Linked matches",
|
dataConnections: "Linked matches",
|
||||||
dataTopicTitle: "Everything related to",
|
|
||||||
dataTopicHint: "What the algorithm relates to this topic",
|
|
||||||
dataNoMatches: "No cross-data to show.",
|
dataNoMatches: "No cross-data to show.",
|
||||||
dataNoProfile: "Publish content or write your CV so the algorithm can learn what interests you.",
|
dataNoProfile: "Publish content or write your CV so the algorithm can learn what interests you.",
|
||||||
pollsDescription: "Module to ask the network and count the answers.",
|
pollsDescription: "Module to ask the network and count the answers.",
|
||||||
|
|
@ -2461,9 +2474,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "engagement",
|
tribeTrendingEngagement: "engagement",
|
||||||
tribeTopCategory: "More Voted",
|
tribeTopCategory: "More Voted",
|
||||||
tribeTagsEmpty: "No tags found.",
|
tribeTagsEmpty: "No tags found.",
|
||||||
tribeSearchPlaceholder: "Search tribe content...",
|
|
||||||
tribeSearchEmpty: "No results found.",
|
tribeSearchEmpty: "No results found.",
|
||||||
tribeSearchResults: "Results",
|
|
||||||
tribeSearchMinChars: "Enter at least 2 characters to search.",
|
tribeSearchMinChars: "Enter at least 2 characters to search.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Here you can find all your assigned items.",
|
agendaDescription: "Here you can find all your assigned items.",
|
||||||
|
|
@ -4089,6 +4100,7 @@ module.exports = {
|
||||||
roomUpdate: "Update",
|
roomUpdate: "Update",
|
||||||
roomDelete: "Delete",
|
roomDelete: "Delete",
|
||||||
roomClose: "Close",
|
roomClose: "Close",
|
||||||
|
roomReopen: "Reopen",
|
||||||
roomCreateSectionTitle: "Create Room",
|
roomCreateSectionTitle: "Create Room",
|
||||||
roomUpdateSectionTitle: "Update Room",
|
roomUpdateSectionTitle: "Update Room",
|
||||||
roomTitlePlaceholder: "Room title",
|
roomTitlePlaceholder: "Room title",
|
||||||
|
|
@ -4107,6 +4119,15 @@ module.exports = {
|
||||||
roomMuted: "muted",
|
roomMuted: "muted",
|
||||||
roomRec: "Record audio",
|
roomRec: "Record audio",
|
||||||
roomRecStop: "Stop recording",
|
roomRecStop: "Stop recording",
|
||||||
|
roomStreamFile: "Stream",
|
||||||
|
roomStreamStop: "Stop",
|
||||||
|
roomStreaming: "Streaming",
|
||||||
|
liveFullscreen: "Full screen",
|
||||||
|
roomStreamPickTitle: "Choose a video",
|
||||||
|
roomStreamPickHint: "Videos on this device. The one you choose is streamed from where it is, without copying or publishing it.",
|
||||||
|
roomStreamGo: "Play in the room",
|
||||||
|
roomStreamNotFound: "That video is not in your folders.",
|
||||||
|
roomPublicWarning: "This room is broadcast on the public web: anyone can see and hear what happens in it.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4649,7 +4670,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "History",
|
phoneHistoryTitle: "History",
|
||||||
phoneLatestTitle: "Latest Calls",
|
phoneLatestTitle: "Latest Calls",
|
||||||
phoneCreateButton: "New call",
|
phoneCreateButton: "New call",
|
||||||
phoneLimitsHint: "Up to 7 inhabitants per call; more than one makes a joint call.",
|
phoneLimitsHint: "Up to {n} inhabitants per call; more than one makes a joint call.",
|
||||||
phoneErrorPub: "Pubs cannot be called.",
|
phoneErrorPub: "Pubs cannot be called.",
|
||||||
phoneJointCall: "Joint call",
|
phoneJointCall: "Joint call",
|
||||||
phoneWith: "With",
|
phoneWith: "With",
|
||||||
|
|
@ -4794,6 +4815,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configure the level of exposition to private messages that you want to have in the network.",
|
settingsPmVisibilityDesc: "Configure the level of exposition to private messages that you want to have in the network.",
|
||||||
settingsPmVisibilityWhole: "Multiverse",
|
settingsPmVisibilityWhole: "Multiverse",
|
||||||
settingsPmVisibilityMutuals: "Only mutual-support",
|
settingsPmVisibilityMutuals: "Only mutual-support",
|
||||||
|
mute: "Mute",
|
||||||
|
unmute: "Unmute",
|
||||||
|
relationshipMuted: "Muted by you",
|
||||||
|
hideContent: "Hide for me",
|
||||||
|
unhideContent: "Show again",
|
||||||
|
hiddenByYou: "Hidden by you",
|
||||||
|
settingsTabContent: "Content",
|
||||||
|
settingsHiddenTitle: "Hidden Content",
|
||||||
|
settingsHiddenDesc: "Inhabitants you muted and content you hid.",
|
||||||
|
settingsMutedLabel: "Muted inhabitants",
|
||||||
|
settingsHiddenLabel: "Hidden content",
|
||||||
|
settingsMutedEmpty: "Nobody muted.",
|
||||||
|
settingsHiddenEmpty: "Nothing hidden.",
|
||||||
|
settingsPmVisibilityFollowing: "Only people I follow",
|
||||||
|
settingsMentionsVisibilityTitle: "Mentions",
|
||||||
|
settingsMentionsVisibilityDesc: "Choose whose mentions reach you.",
|
||||||
pmMutualNotice: "Outbound is a UX guardrail. Inbound is an attention filter (ciphertext is still replicated).",
|
pmMutualNotice: "Outbound is a UX guardrail. Inbound is an attention filter (ciphertext is still replicated).",
|
||||||
inhabitantsPendingFollowsTitle: "Pending follow requests",
|
inhabitantsPendingFollowsTitle: "Pending follow requests",
|
||||||
inhabitantsPendingAccept: "Accept",
|
inhabitantsPendingAccept: "Accept",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Accesible por Tor",
|
peersTorYes: "Accesible por Tor",
|
||||||
peersTorNo: "No accesible por Tor",
|
peersTorNo: "No accesible por Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Versión",
|
peersOasisVersion: "Versión",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Mapa interactivo de la red alrededor de ti.",
|
graphosDescription: "Mapa interactivo de la red alrededor de ti.",
|
||||||
|
|
@ -594,6 +594,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "No anuncia su versión: un Oasis antiguo",
|
peerVersionUnannounced: "No anuncia su versión: un Oasis antiguo",
|
||||||
peersTorMissing: "Este PUB solamente permite conexiones desde TOR.",
|
peersTorMissing: "Este PUB solamente permite conexiones desde TOR.",
|
||||||
peerConnectionsTitle: "Conexiones",
|
peerConnectionsTitle: "Conexiones",
|
||||||
|
peersOnlineHint: "Nodos conectados contigo ahora mismo.",
|
||||||
|
peersDiscoveredHint: "Nodos con dirección conocida que no están conectados ahora.",
|
||||||
|
peersUnknownHint: "Nodos de tu grafo social sin dirección conocida.",
|
||||||
|
peersConnectionsHint: "Direcciones a las que puede conectar este nodo y el estado de cada conexión.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Salud",
|
||||||
|
peersHealthHint: "Cómo está cada pub o nodo conocido, en la red local y fuera de ella.",
|
||||||
|
peersHealthReachable: "Alcanzable",
|
||||||
|
peersHealthFailing: "Fallando",
|
||||||
|
peersHealthFailures: "Fallos",
|
||||||
|
peersFailingSince: "Falla desde",
|
||||||
|
peersHealthForget: "Olvidar inalcanzables",
|
||||||
|
peersLanHint: "Nodos encontrados en tu red local.",
|
||||||
|
peersLanLastSeen: "Visto por última vez",
|
||||||
|
peersLanEmpty: "Aún no se ha encontrado ningún nodo en la red local.",
|
||||||
online: "Conectado",
|
online: "Conectado",
|
||||||
offline: "Desconectado",
|
offline: "Desconectado",
|
||||||
discovered: 'Descubiertos',
|
discovered: 'Descubiertos',
|
||||||
|
|
@ -1639,8 +1654,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Palabras clave",
|
dataTermsTitle: "Palabras clave",
|
||||||
dataTermsHint: "Los términos que aparecen en más contenidos.",
|
dataTermsHint: "Los términos que aparecen en más contenidos.",
|
||||||
dataConnections: "Coincidencias enlazadas",
|
dataConnections: "Coincidencias enlazadas",
|
||||||
dataTopicTitle: "Todo lo relacionado con",
|
|
||||||
dataTopicHint: "Lo que el algoritmo relaciona con este tema",
|
|
||||||
dataNoMatches: "No hay datos cruzados que mostrar.",
|
dataNoMatches: "No hay datos cruzados que mostrar.",
|
||||||
dataNoProfile: "Publica contenido o escribe tu CV para que el algoritmo aprenda qué te interesa.",
|
dataNoProfile: "Publica contenido o escribe tu CV para que el algoritmo aprenda qué te interesa.",
|
||||||
pollsDescription: "Módulo para preguntar a la red y contar las respuestas.",
|
pollsDescription: "Módulo para preguntar a la red y contar las respuestas.",
|
||||||
|
|
@ -2194,9 +2207,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "interacción",
|
tribeTrendingEngagement: "interacción",
|
||||||
tribeTopCategory: "Más votado",
|
tribeTopCategory: "Más votado",
|
||||||
tribeTagsEmpty: "No se encontraron etiquetas.",
|
tribeTagsEmpty: "No se encontraron etiquetas.",
|
||||||
tribeSearchPlaceholder: "Buscar contenido de la tribu...",
|
|
||||||
tribeSearchEmpty: "Sin resultados.",
|
tribeSearchEmpty: "Sin resultados.",
|
||||||
tribeSearchResults: "Resultados",
|
|
||||||
tribeSearchMinChars: "Introduce al menos 2 caracteres para buscar.",
|
tribeSearchMinChars: "Introduce al menos 2 caracteres para buscar.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Aquí puedes encontrar todos tus elementos asignados.",
|
agendaDescription: "Aquí puedes encontrar todos tus elementos asignados.",
|
||||||
|
|
@ -3833,6 +3844,7 @@ module.exports = {
|
||||||
roomUpdate: "Actualizar",
|
roomUpdate: "Actualizar",
|
||||||
roomDelete: "Eliminar",
|
roomDelete: "Eliminar",
|
||||||
roomClose: "Cerrar",
|
roomClose: "Cerrar",
|
||||||
|
roomReopen: "Reabrir",
|
||||||
roomCreateSectionTitle: "Crear sala",
|
roomCreateSectionTitle: "Crear sala",
|
||||||
roomUpdateSectionTitle: "Actualizar sala",
|
roomUpdateSectionTitle: "Actualizar sala",
|
||||||
roomTitlePlaceholder: "Título de la sala",
|
roomTitlePlaceholder: "Título de la sala",
|
||||||
|
|
@ -3851,6 +3863,15 @@ module.exports = {
|
||||||
roomMuted: "silenciado",
|
roomMuted: "silenciado",
|
||||||
roomRec: "Grabar audio",
|
roomRec: "Grabar audio",
|
||||||
roomRecStop: "Parar grabación",
|
roomRecStop: "Parar grabación",
|
||||||
|
roomStreamFile: "Emitir",
|
||||||
|
roomStreamStop: "Parar",
|
||||||
|
roomStreaming: "Emitiendo",
|
||||||
|
liveFullscreen: "Pantalla completa",
|
||||||
|
roomStreamPickTitle: "Elige un vídeo",
|
||||||
|
roomStreamPickHint: "Vídeos de este dispositivo. El que elijas se emite desde donde está, sin copiarlo ni publicarlo.",
|
||||||
|
roomStreamGo: "Emitir en la sala",
|
||||||
|
roomStreamNotFound: "Ese vídeo no está en tus carpetas.",
|
||||||
|
roomPublicWarning: "Esta sala se retransmite en la web pública: cualquiera puede ver y oír lo que pasa en ella.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4332,7 +4353,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Historial",
|
phoneHistoryTitle: "Historial",
|
||||||
phoneLatestTitle: "Últimas llamadas",
|
phoneLatestTitle: "Últimas llamadas",
|
||||||
phoneCreateButton: "Nueva llamada",
|
phoneCreateButton: "Nueva llamada",
|
||||||
phoneLimitsHint: "Hasta 7 habitantes por llamada; con más de uno es una llamada conjunta.",
|
phoneLimitsHint: "Hasta {n} habitantes por llamada; con más de uno es una llamada conjunta.",
|
||||||
phoneErrorPub: "No se puede llamar a un pub.",
|
phoneErrorPub: "No se puede llamar a un pub.",
|
||||||
phoneJointCall: "Llamada conjunta",
|
phoneJointCall: "Llamada conjunta",
|
||||||
phoneWith: "Con",
|
phoneWith: "Con",
|
||||||
|
|
@ -4477,6 +4498,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configura el nivel de exposición a mensajes privados que deseas tener en la red.",
|
settingsPmVisibilityDesc: "Configura el nivel de exposición a mensajes privados que deseas tener en la red.",
|
||||||
settingsPmVisibilityWhole: "Multiverso",
|
settingsPmVisibilityWhole: "Multiverso",
|
||||||
settingsPmVisibilityMutuals: "Solo soporte mutuo",
|
settingsPmVisibilityMutuals: "Solo soporte mutuo",
|
||||||
|
mute: "Silenciar",
|
||||||
|
unmute: "Dejar de silenciar",
|
||||||
|
relationshipMuted: "Silenciado por ti",
|
||||||
|
hideContent: "Ocultar para mí",
|
||||||
|
unhideContent: "Mostrar de nuevo",
|
||||||
|
hiddenByYou: "Ocultado por ti",
|
||||||
|
settingsTabContent: "Contenido",
|
||||||
|
settingsHiddenTitle: "Contenido oculto",
|
||||||
|
settingsHiddenDesc: "Habitantes que has silenciado y contenido que has ocultado.",
|
||||||
|
settingsMutedLabel: "Habitantes silenciados",
|
||||||
|
settingsHiddenLabel: "Contenido oculto",
|
||||||
|
settingsMutedEmpty: "Nadie silenciado.",
|
||||||
|
settingsHiddenEmpty: "Nada oculto.",
|
||||||
|
settingsPmVisibilityFollowing: "Solo gente a la que sigo",
|
||||||
|
settingsMentionsVisibilityTitle: "Menciones",
|
||||||
|
settingsMentionsVisibilityDesc: "Elige de quién te llegan las menciones.",
|
||||||
pmMutualNotice: "El envío es una barrera de UX. La recepción es un filtro de atención (el cifrado ya está replicado).",
|
pmMutualNotice: "El envío es una barrera de UX. La recepción es un filtro de atención (el cifrado ya está replicado).",
|
||||||
inhabitantsPendingFollowsTitle: "Solicitudes de apoyo pendientes",
|
inhabitantsPendingFollowsTitle: "Solicitudes de apoyo pendientes",
|
||||||
inhabitantsPendingAccept: "Aceptar",
|
inhabitantsPendingAccept: "Aceptar",
|
||||||
|
|
@ -4583,7 +4620,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "ECO Tax(habitante) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "ECO Tax(habitante) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "El impuesto ECO se deduce de la RBU bruta cuando se calcula la asignación de la época. Los ECO deducidos no se redistribuyen en esta época — permanecen en el saldo del pub y alimentan el fondo de la próxima época a través de los topes reserveMin / alpha.",
|
bankRulesEcoTaxNote: "El impuesto ECO se deduce de la RBU bruta cuando se calcula la asignación de la época. Los ECO deducidos no se redistribuyen en esta época — permanecen en el saldo del pub y alimentan el fondo de la próxima época a través de los topes reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
||||||
bankRulesKarmaNote: "El karma se deriva de tus acciones (publicaciones, votos, participación en el parlamento/tribunales, etc.) con un factor de decaimiento temporal, menos los gramos de carbono que genera tu feed.",
|
bankRulesKarmaNote: "El karma se deriva de tus acciones (publicaciones, votos, participación en el parlamento/tribunales, etc.) con un factor de decaimiento temporal, menos los gramos de carbono que genera tu feed. Lo que recibe tu contenido también cuenta: las opiniones, votos, reacciones y reproducciones positivas suman karma, y las opiniones o votos negativos lo restan cuando varios habitantes coinciden.",
|
||||||
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
||||||
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
||||||
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
||||||
|
|
@ -4618,7 +4655,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "En el test WILL cada opción pondera una o más casas; al enviarlo, el sistema suma las puntuaciones y te admite automáticamente en la casa con más puntos.",
|
larpRulesTestEntryText: "En el test WILL cada opción pondera una o más casas; al enviarlo, el sistema suma las puntuaciones y te admite automáticamente en la casa con más puntos.",
|
||||||
larpWillTestHint: "Una vez completado, se te asignará la casa que más se ajuste a tus respuestas. Tus respuestas individuales se procesan localmente y nunca se almacenan — solo se publica en tu feed la asignación de casa resultante.",
|
larpWillTestHint: "Una vez completado, se te asignará la casa que más se ajuste a tus respuestas. Tus respuestas individuales se procesan localmente y nunca se almacenan — solo se publica en tu feed la asignación de casa resultante.",
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local. Desactivar para detener las difusiones UDP.",
|
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",
|
||||||
settingsLanTitle: "Difusión LAN",
|
settingsLanTitle: "Difusión LAN",
|
||||||
settingsReplicationTitle: "Replicación",
|
settingsReplicationTitle: "Replicación",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Tor bidez irisgarria",
|
peersTorYes: "Tor bidez irisgarria",
|
||||||
peersTorNo: "Ez da Tor bidez irisgarria",
|
peersTorNo: "Ez da Tor bidez irisgarria",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Bertsioa",
|
peersOasisVersion: "Bertsioa",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Sarearen mapa interaktiboa zure inguruan.",
|
graphosDescription: "Sarearen mapa interaktiboa zure inguruan.",
|
||||||
|
|
@ -596,6 +596,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Ez du bere bertsioa iragartzen: Oasis zahar bat",
|
peerVersionUnannounced: "Ez du bere bertsioa iragartzen: Oasis zahar bat",
|
||||||
peersTorMissing: "PUB honek TOR bidezko konexioak soilik onartzen ditu.",
|
peersTorMissing: "PUB honek TOR bidezko konexioak soilik onartzen ditu.",
|
||||||
peerConnectionsTitle: "Konexioak",
|
peerConnectionsTitle: "Konexioak",
|
||||||
|
peersOnlineHint: "Orain zurekin konektatuta dauden nodoak.",
|
||||||
|
peersDiscoveredHint: "Helbide ezaguna duten baina orain konektatuta ez dauden nodoak.",
|
||||||
|
peersUnknownHint: "Zure grafo sozialeko nodoak, helbide ezagunik gabe.",
|
||||||
|
peersConnectionsHint: "Nodo honek dei ditzakeen helbideak eta konexio bakoitzaren egoera.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Osasuna",
|
||||||
|
peersHealthHint: "Nola dagoen pub edo nodo ezagun bakoitza, sare lokalean eta kanpoan.",
|
||||||
|
peersHealthReachable: "Eskuragarri",
|
||||||
|
peersHealthFailing: "Huts egiten",
|
||||||
|
peersHealthFailures: "Hutsak",
|
||||||
|
peersFailingSince: "Huts egiten noiztik",
|
||||||
|
peersHealthForget: "Ahaztu eskuraezinak",
|
||||||
|
peersLanHint: "Zure sare lokalean aurkitutako nodoak.",
|
||||||
|
peersLanLastSeen: "Azkenekoz ikusia",
|
||||||
|
peersLanEmpty: "Oraindik ez da nodorik aurkitu sare lokalean.",
|
||||||
online: "Linean",
|
online: "Linean",
|
||||||
offline: "Lineaz kanpo",
|
offline: "Lineaz kanpo",
|
||||||
discovered: 'Aurkituak',
|
discovered: 'Aurkituak',
|
||||||
|
|
@ -1680,8 +1695,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Gako-hitzak",
|
dataTermsTitle: "Gako-hitzak",
|
||||||
dataTermsHint: "Eduki gehienetan agertzen diren terminoak.",
|
dataTermsHint: "Eduki gehienetan agertzen diren terminoak.",
|
||||||
dataConnections: "Lotutako bat-etortzeak",
|
dataConnections: "Lotutako bat-etortzeak",
|
||||||
dataTopicTitle: "Honekin lotutako guztia:",
|
|
||||||
dataTopicHint: "Algoritmoak gai honekin lotzen duena",
|
|
||||||
dataNoMatches: "Ez dago datu gurutzaturik erakusteko.",
|
dataNoMatches: "Ez dago datu gurutzaturik erakusteko.",
|
||||||
dataNoProfile: "Argitaratu edukia edo idatzi zure CVa, algoritmoak zure interesak ikas ditzan.",
|
dataNoProfile: "Argitaratu edukia edo idatzi zure CVa, algoritmoak zure interesak ikas ditzan.",
|
||||||
pollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.",
|
pollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.",
|
||||||
|
|
@ -2198,9 +2211,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "parte-hartzea",
|
tribeTrendingEngagement: "parte-hartzea",
|
||||||
tribeTopCategory: "Gehien bozkatua",
|
tribeTopCategory: "Gehien bozkatua",
|
||||||
tribeTagsEmpty: "Ez da etiketarik aurkitu.",
|
tribeTagsEmpty: "Ez da etiketarik aurkitu.",
|
||||||
tribeSearchPlaceholder: "Bilatu tribuaren edukian...",
|
|
||||||
tribeSearchEmpty: "Emaitzarik ez.",
|
tribeSearchEmpty: "Emaitzarik ez.",
|
||||||
tribeSearchResults: "Emaitzak",
|
|
||||||
tribeSearchMinChars: "Idatzi gutxienez 2 karaktere bilatzeko.",
|
tribeSearchMinChars: "Idatzi gutxienez 2 karaktere bilatzeko.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Eskeituta dauzkazun elementu guztiak aurki ditzakezu hemen.",
|
agendaDescription: "Eskeituta dauzkazun elementu guztiak aurki ditzakezu hemen.",
|
||||||
|
|
@ -3833,6 +3844,7 @@ module.exports = {
|
||||||
roomUpdate: "Eguneratu",
|
roomUpdate: "Eguneratu",
|
||||||
roomDelete: "Ezabatu",
|
roomDelete: "Ezabatu",
|
||||||
roomClose: "Itxi",
|
roomClose: "Itxi",
|
||||||
|
roomReopen: "Berriro ireki",
|
||||||
roomCreateSectionTitle: "Sortu gela",
|
roomCreateSectionTitle: "Sortu gela",
|
||||||
roomUpdateSectionTitle: "Eguneratu gela",
|
roomUpdateSectionTitle: "Eguneratu gela",
|
||||||
roomTitlePlaceholder: "Gelaren izenburua",
|
roomTitlePlaceholder: "Gelaren izenburua",
|
||||||
|
|
@ -3851,6 +3863,15 @@ module.exports = {
|
||||||
roomMuted: "isilduta",
|
roomMuted: "isilduta",
|
||||||
roomRec: "Audioa grabatu",
|
roomRec: "Audioa grabatu",
|
||||||
roomRecStop: "Grabazioa gelditu",
|
roomRecStop: "Grabazioa gelditu",
|
||||||
|
roomStreamFile: "Igorri",
|
||||||
|
roomStreamStop: "Gelditu",
|
||||||
|
roomStreaming: "Igortzen",
|
||||||
|
liveFullscreen: "Pantaila osoa",
|
||||||
|
roomStreamPickTitle: "Aukeratu bideo bat",
|
||||||
|
roomStreamPickHint: "Gailu honetako bideoak. Aukeratzen duzuna dagoen tokitik igortzen da, kopiatu edo argitaratu gabe.",
|
||||||
|
roomStreamGo: "Gelan igorri",
|
||||||
|
roomStreamNotFound: "Bideo hori ez dago zure karpetetan.",
|
||||||
|
roomPublicWarning: "Gela hau web publikoan igortzen da: edonork ikus eta entzun dezake bertan gertatzen dena.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4331,7 +4352,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Historia",
|
phoneHistoryTitle: "Historia",
|
||||||
phoneLatestTitle: "Azken deiak",
|
phoneLatestTitle: "Azken deiak",
|
||||||
phoneCreateButton: "Dei berria",
|
phoneCreateButton: "Dei berria",
|
||||||
phoneLimitsHint: "Dei bakoitzeko 7 bizilagun gehienez; bat baino gehiagorekin dei bateratua da.",
|
phoneLimitsHint: "Dei bakoitzeko {n} bizilagun gehienez; bat baino gehiagorekin dei bateratua da.",
|
||||||
phoneErrorPub: "Ezin zaio pub bati deitu.",
|
phoneErrorPub: "Ezin zaio pub bati deitu.",
|
||||||
phoneJointCall: "Dei bateratua",
|
phoneJointCall: "Dei bateratua",
|
||||||
phoneWith: "Norekin",
|
phoneWith: "Norekin",
|
||||||
|
|
@ -4476,6 +4497,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Konfiguratu sarean izan nahi duzun mezu pribatuen esposizio maila.",
|
settingsPmVisibilityDesc: "Konfiguratu sarean izan nahi duzun mezu pribatuen esposizio maila.",
|
||||||
settingsPmVisibilityWhole: "Multibertsoa",
|
settingsPmVisibilityWhole: "Multibertsoa",
|
||||||
settingsPmVisibilityMutuals: "Elkar laguntza soilik",
|
settingsPmVisibilityMutuals: "Elkar laguntza soilik",
|
||||||
|
mute: "Isilarazi",
|
||||||
|
unmute: "Isilarazteari utzi",
|
||||||
|
relationshipMuted: "Zuk isilarazia",
|
||||||
|
hideContent: "Niretzat ezkutatu",
|
||||||
|
unhideContent: "Berriro erakutsi",
|
||||||
|
hiddenByYou: "Zuk ezkutatua",
|
||||||
|
settingsTabContent: "Edukia",
|
||||||
|
settingsHiddenTitle: "Ezkutatutako edukia",
|
||||||
|
settingsHiddenDesc: "Isilarazi dituzun biztanleak eta ezkutatu duzun edukia.",
|
||||||
|
settingsMutedLabel: "Isilarazitako biztanleak",
|
||||||
|
settingsHiddenLabel: "Ezkutatutako edukia",
|
||||||
|
settingsMutedEmpty: "Inor ez isilarazita.",
|
||||||
|
settingsHiddenEmpty: "Ezer ez ezkutatuta.",
|
||||||
|
settingsPmVisibilityFollowing: "Jarraitzen dudan jendea bakarrik",
|
||||||
|
settingsMentionsVisibilityTitle: "Aipamenak",
|
||||||
|
settingsMentionsVisibilityDesc: "Aukeratu noren aipamenak iristen zaizkizun.",
|
||||||
pmMutualNotice: "Bidalketa UX babes bat da. Jasotzea arreta-iragazkia.",
|
pmMutualNotice: "Bidalketa UX babes bat da. Jasotzea arreta-iragazkia.",
|
||||||
inhabitantsPendingFollowsTitle: "Onespen zain dauden eskaerak",
|
inhabitantsPendingFollowsTitle: "Onespen zain dauden eskaerak",
|
||||||
inhabitantsPendingAccept: "Onartu",
|
inhabitantsPendingAccept: "Onartu",
|
||||||
|
|
@ -4608,7 +4645,7 @@ module.exports = {
|
||||||
bankRulesFloor: "Erabiltzaile bakoitzeko gutxienekoa (gordina)",
|
bankRulesFloor: "Erabiltzaile bakoitzeko gutxienekoa (gordina)",
|
||||||
bankRulesGraceDays: "Eskaeraren grazia-epea",
|
bankRulesGraceDays: "Eskaeraren grazia-epea",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "Karma zure ekintzetatik ateratzen da (argitalpenak, botoak, parlamentuan/epaitegietan parte hartzea, etab.), denborarekin ahultzen den faktore batekin, zure jarioak sortutako karbono-gramoak kenduta.",
|
bankRulesKarmaNote: "Karma zure ekintzetatik ateratzen da (argitalpenak, botoak, parlamentuan/epaitegietan parte hartzea, etab.), denborarekin ahultzen den faktore batekin, zure jarioak sortutako karbono-gramoak kenduta. Zure edukiak jasotzen duenak ere balio du: iritzi, boto, erreakzio eta erreprodukzio positiboek karma gehitzen dute, eta iritzi edo boto negatiboek kentzen dute hainbat biztanle ados daudenean.",
|
||||||
bankRulesKarmaTitle: "Karma (erabiltzailearen konpromiso-puntuazioa)",
|
bankRulesKarmaTitle: "Karma (erabiltzailearen konpromiso-puntuazioa)",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
|
|
@ -4697,7 +4734,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "WILL testean aukera bakoitzak etxe bat edo gehiago haztatzen ditu; bidaltzean, sistemak puntuazioak batzen ditu eta puntu gehien dituen etxean onartzen zaitu automatikoki.",
|
larpRulesTestEntryText: "WILL testean aukera bakoitzak etxe bat edo gehiago haztatzen ditu; bidaltzean, sistemak puntuazioak batzen ditu eta puntu gehien dituen etxean onartzen zaitu automatikoki.",
|
||||||
larpWillTestHint: "Behin osatutakoan, zure erantzunekin ondoen bat datorren etxera esleituko zaitu. Zure erantzun indibidualak lokalki prozesatzen dira eta inoiz ez dira gordetzen — soilik etxe-esleipenaren emaitza argitaratzen da zure jarioan.",
|
larpWillTestHint: "Behin osatutakoan, zure erantzunekin ondoen bat datorren etxera esleituko zaitu. Zure erantzun indibidualak lokalki prozesatzen dira eta inoiz ez dira gordetzen — soilik etxe-esleipenaren emaitza argitaratzen da zure jarioan.",
|
||||||
larpWillTestTitle: "WILL proba",
|
larpWillTestTitle: "WILL proba",
|
||||||
settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei. Desgaitu UDP igorpenak gelditzeko.",
|
settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei.",
|
||||||
settingsLanEnable: "Gaitu LAN igorpena",
|
settingsLanEnable: "Gaitu LAN igorpena",
|
||||||
settingsLanTitle: "LAN igorpena",
|
settingsLanTitle: "LAN igorpena",
|
||||||
settingsReplicationTitle: "Erreplikazioa",
|
settingsReplicationTitle: "Erreplikazioa",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Joignable via Tor",
|
peersTorYes: "Joignable via Tor",
|
||||||
peersTorNo: "Non joignable via Tor",
|
peersTorNo: "Non joignable via Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Version",
|
peersOasisVersion: "Version",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Carte interactive du réseau autour de toi.",
|
graphosDescription: "Carte interactive du réseau autour de toi.",
|
||||||
|
|
@ -594,6 +594,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "N'annonce pas sa version : un ancien Oasis",
|
peerVersionUnannounced: "N'annonce pas sa version : un ancien Oasis",
|
||||||
peersTorMissing: "Ce PUB n'accepte que des connexions via TOR.",
|
peersTorMissing: "Ce PUB n'accepte que des connexions via TOR.",
|
||||||
peerConnectionsTitle: "Connexions",
|
peerConnectionsTitle: "Connexions",
|
||||||
|
peersOnlineHint: "Nœuds connectés à vous en ce moment.",
|
||||||
|
peersDiscoveredHint: "Nœuds avec une adresse connue, non connectés pour l'instant.",
|
||||||
|
peersUnknownHint: "Nœuds de votre graphe social sans adresse connue.",
|
||||||
|
peersConnectionsHint: "Adresses que ce nœud peut joindre et l'état de chaque connexion.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Santé",
|
||||||
|
peersHealthHint: "L'état de chaque pub ou pair connu, sur le réseau local et au-delà.",
|
||||||
|
peersHealthReachable: "Joignable",
|
||||||
|
peersHealthFailing: "En échec",
|
||||||
|
peersHealthFailures: "Échecs",
|
||||||
|
peersFailingSince: "En échec depuis",
|
||||||
|
peersHealthForget: "Oublier les injoignables",
|
||||||
|
peersLanHint: "Nœuds rencontrés sur votre réseau local.",
|
||||||
|
peersLanLastSeen: "Vu pour la dernière fois",
|
||||||
|
peersLanEmpty: "Aucun nœud rencontré sur le réseau local pour l'instant.",
|
||||||
online: "En ligne",
|
online: "En ligne",
|
||||||
offline: "Hors ligne",
|
offline: "Hors ligne",
|
||||||
discovered: 'Découverts',
|
discovered: 'Découverts',
|
||||||
|
|
@ -1638,8 +1653,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Mots-clés",
|
dataTermsTitle: "Mots-clés",
|
||||||
dataTermsHint: "Les termes présents dans le plus de contenus.",
|
dataTermsHint: "Les termes présents dans le plus de contenus.",
|
||||||
dataConnections: "Correspondances liées",
|
dataConnections: "Correspondances liées",
|
||||||
dataTopicTitle: "Tout ce qui touche à",
|
|
||||||
dataTopicHint: "Ce que l'algorithme relie à ce sujet",
|
|
||||||
dataNoMatches: "Aucune donnée croisée à afficher.",
|
dataNoMatches: "Aucune donnée croisée à afficher.",
|
||||||
dataNoProfile: "Publiez du contenu ou rédigez votre CV pour que l'algorithme apprenne ce qui vous intéresse.",
|
dataNoProfile: "Publiez du contenu ou rédigez votre CV pour que l'algorithme apprenne ce qui vous intéresse.",
|
||||||
pollsDescription: "Module pour interroger le réseau et compter les réponses.",
|
pollsDescription: "Module pour interroger le réseau et compter les réponses.",
|
||||||
|
|
@ -2187,9 +2200,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "engagement",
|
tribeTrendingEngagement: "engagement",
|
||||||
tribeTopCategory: "Plus voté",
|
tribeTopCategory: "Plus voté",
|
||||||
tribeTagsEmpty: "Aucun tag trouvé.",
|
tribeTagsEmpty: "Aucun tag trouvé.",
|
||||||
tribeSearchPlaceholder: "Rechercher dans la tribu...",
|
|
||||||
tribeSearchEmpty: "Aucun résultat.",
|
tribeSearchEmpty: "Aucun résultat.",
|
||||||
tribeSearchResults: "Résultats",
|
|
||||||
tribeSearchMinChars: "Entrez au moins 2 caractères pour rechercher.",
|
tribeSearchMinChars: "Entrez au moins 2 caractères pour rechercher.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Ici vous pouvez trouver tous vos éléments assignés.",
|
agendaDescription: "Ici vous pouvez trouver tous vos éléments assignés.",
|
||||||
|
|
@ -3825,6 +3836,7 @@ module.exports = {
|
||||||
roomUpdate: "Modifier",
|
roomUpdate: "Modifier",
|
||||||
roomDelete: "Supprimer",
|
roomDelete: "Supprimer",
|
||||||
roomClose: "Fermer",
|
roomClose: "Fermer",
|
||||||
|
roomReopen: "Rouvrir",
|
||||||
roomCreateSectionTitle: "Créer un salon",
|
roomCreateSectionTitle: "Créer un salon",
|
||||||
roomUpdateSectionTitle: "Modifier le salon",
|
roomUpdateSectionTitle: "Modifier le salon",
|
||||||
roomTitlePlaceholder: "Titre du salon",
|
roomTitlePlaceholder: "Titre du salon",
|
||||||
|
|
@ -3843,6 +3855,15 @@ module.exports = {
|
||||||
roomMuted: "micro coupé",
|
roomMuted: "micro coupé",
|
||||||
roomRec: "Enregistrer l'audio",
|
roomRec: "Enregistrer l'audio",
|
||||||
roomRecStop: "Arrêter l'enregistrement",
|
roomRecStop: "Arrêter l'enregistrement",
|
||||||
|
roomStreamFile: "Diffuser",
|
||||||
|
roomStreamStop: "Arrêter",
|
||||||
|
roomStreaming: "En diffusion",
|
||||||
|
liveFullscreen: "Plein écran",
|
||||||
|
roomStreamPickTitle: "Choisir une vidéo",
|
||||||
|
roomStreamPickHint: "Vidéos de cet appareil. Celle que vous choisissez est diffusée depuis son emplacement, sans être copiée ni publiée.",
|
||||||
|
roomStreamGo: "Diffuser dans le salon",
|
||||||
|
roomStreamNotFound: "Cette vidéo n'est pas dans vos dossiers.",
|
||||||
|
roomPublicWarning: "Ce salon est diffusé sur le web public : tout le monde peut voir et entendre ce qui s'y passe.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4323,7 +4344,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Historique",
|
phoneHistoryTitle: "Historique",
|
||||||
phoneLatestTitle: "Derniers appels",
|
phoneLatestTitle: "Derniers appels",
|
||||||
phoneCreateButton: "Nouvel appel",
|
phoneCreateButton: "Nouvel appel",
|
||||||
phoneLimitsHint: "Jusqu'à 7 habitants par appel ; à plus d'un, c'est un appel conjoint.",
|
phoneLimitsHint: "Jusqu'à {n} habitants par appel ; à plus d'un, c'est un appel conjoint.",
|
||||||
phoneErrorPub: "Impossible d'appeler un pub.",
|
phoneErrorPub: "Impossible d'appeler un pub.",
|
||||||
phoneJointCall: "Appel conjoint",
|
phoneJointCall: "Appel conjoint",
|
||||||
phoneWith: "Avec",
|
phoneWith: "Avec",
|
||||||
|
|
@ -4468,6 +4489,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configurez votre niveau d'exposition aux messages privés.",
|
settingsPmVisibilityDesc: "Configurez votre niveau d'exposition aux messages privés.",
|
||||||
settingsPmVisibilityWhole: "Multivers",
|
settingsPmVisibilityWhole: "Multivers",
|
||||||
settingsPmVisibilityMutuals: "Soutien mutuel uniquement",
|
settingsPmVisibilityMutuals: "Soutien mutuel uniquement",
|
||||||
|
mute: "Mettre en sourdine",
|
||||||
|
unmute: "Réactiver",
|
||||||
|
relationshipMuted: "En sourdine pour vous",
|
||||||
|
hideContent: "Masquer pour moi",
|
||||||
|
unhideContent: "Afficher à nouveau",
|
||||||
|
hiddenByYou: "Masqué par vous",
|
||||||
|
settingsTabContent: "Contenu",
|
||||||
|
settingsHiddenTitle: "Contenu masqué",
|
||||||
|
settingsHiddenDesc: "Habitants mis en sourdine et contenus masqués.",
|
||||||
|
settingsMutedLabel: "Habitants en sourdine",
|
||||||
|
settingsHiddenLabel: "Contenu masqué",
|
||||||
|
settingsMutedEmpty: "Personne en sourdine.",
|
||||||
|
settingsHiddenEmpty: "Rien de masqué.",
|
||||||
|
settingsPmVisibilityFollowing: "Seulement les personnes que je suis",
|
||||||
|
settingsMentionsVisibilityTitle: "Mentions",
|
||||||
|
settingsMentionsVisibilityDesc: "Choisissez de qui vous recevez des mentions.",
|
||||||
pmMutualNotice: "L'envoi est un garde-fou UX. La réception est un filtre d'attention.",
|
pmMutualNotice: "L'envoi est un garde-fou UX. La réception est un filtre d'attention.",
|
||||||
inhabitantsPendingFollowsTitle: "Demandes de soutien en attente",
|
inhabitantsPendingFollowsTitle: "Demandes de soutien en attente",
|
||||||
inhabitantsPendingAccept: "Accepter",
|
inhabitantsPendingAccept: "Accepter",
|
||||||
|
|
@ -4574,7 +4611,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "La taxe ECO est déduite de la RBU brute lors du calcul de l'allocation de l'époque. Les ECO déduits ne sont pas redistribués pendant cette époque — ils restent dans le solde du pub et alimentent le fonds de l'époque suivante via les plafonds reserveMin / alpha.",
|
bankRulesEcoTaxNote: "La taxe ECO est déduite de la RBU brute lors du calcul de l'allocation de l'époque. Les ECO déduits ne sont pas redistribués pendant cette époque — ils restent dans le solde du pub et alimentent le fonds de l'époque suivante via les plafonds reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "Le karma est calculé à partir de vos actions (publications, votes, participation au parlement/aux tribunaux, etc.) avec un facteur de décroissance temporelle, moins les grammes de carbone générés par votre fil.",
|
bankRulesKarmaNote: "Le karma est calculé à partir de vos actions (publications, votes, participation au parlement/aux tribunaux, etc.) avec un facteur de décroissance temporelle, moins les grammes de carbone générés par votre fil. Ce que reçoit votre contenu compte aussi : les avis, votes, réactions et écoutes positifs ajoutent du karma, et les avis ou votes négatifs en retirent dès que plusieurs habitants sont d'accord.",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
||||||
|
|
@ -4617,7 +4654,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "Dans le test WILL, chaque option pondère une ou plusieurs maisons ; à l'envoi, le système additionne les scores et vous admet automatiquement dans la maison la mieux notée.",
|
larpRulesTestEntryText: "Dans le test WILL, chaque option pondère une ou plusieurs maisons ; à l'envoi, le système additionne les scores et vous admet automatiquement dans la maison la mieux notée.",
|
||||||
larpWillTestHint: "Une fois terminé, on t'assignera la maison qui correspond le mieux à tes réponses. Tes réponses individuelles sont traitées localement et ne sont jamais stockées — seule l'attribution de maison résultante est publiée dans ton flux.",
|
larpWillTestHint: "Une fois terminé, on t'assignera la maison qui correspond le mieux à tes réponses. Tes réponses individuelles sont traitées localement et ne sont jamais stockées — seule l'attribution de maison résultante est publiée dans ton flux.",
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local. Désactiver pour arrêter les diffusions UDP.",
|
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",
|
||||||
settingsLanTitle: "Diffusion LAN",
|
settingsLanTitle: "Diffusion LAN",
|
||||||
settingsReplicationTitle: "Réplication",
|
settingsReplicationTitle: "Réplication",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Accesible por Tor",
|
peersTorYes: "Accesible por Tor",
|
||||||
peersTorNo: "Non accesible por Tor",
|
peersTorNo: "Non accesible por Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Versión",
|
peersOasisVersion: "Versión",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Mapa interactivo da rede ao teu redor.",
|
graphosDescription: "Mapa interactivo da rede ao teu redor.",
|
||||||
|
|
@ -594,6 +594,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Non anuncia a súa versión: un Oasis antigo",
|
peerVersionUnannounced: "Non anuncia a súa versión: un Oasis antigo",
|
||||||
peersTorMissing: "Este PUB só permite conexións dende TOR.",
|
peersTorMissing: "Este PUB só permite conexións dende TOR.",
|
||||||
peerConnectionsTitle: "Conexións",
|
peerConnectionsTitle: "Conexións",
|
||||||
|
peersOnlineHint: "Nodos conectados contigo agora mesmo.",
|
||||||
|
peersDiscoveredHint: "Nodos con enderezo coñecido que non están conectados agora.",
|
||||||
|
peersUnknownHint: "Nodos do teu grafo social sen enderezo coñecido.",
|
||||||
|
peersConnectionsHint: "Enderezos aos que pode conectar este nodo e o estado de cada conexión.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Saúde",
|
||||||
|
peersHealthHint: "Como está cada pub ou nodo coñecido, na rede local e fóra dela.",
|
||||||
|
peersHealthReachable: "Alcanzable",
|
||||||
|
peersHealthFailing: "Fallando",
|
||||||
|
peersHealthFailures: "Fallos",
|
||||||
|
peersFailingSince: "Falla desde",
|
||||||
|
peersHealthForget: "Esquecer inalcanzables",
|
||||||
|
peersLanHint: "Nodos atopados na túa rede local.",
|
||||||
|
peersLanLastSeen: "Visto por última vez",
|
||||||
|
peersLanEmpty: "Aínda non se atopou ningún nodo na rede local.",
|
||||||
online: "Conectado",
|
online: "Conectado",
|
||||||
offline: "Desconectado",
|
offline: "Desconectado",
|
||||||
discovered: "Descubertos",
|
discovered: "Descubertos",
|
||||||
|
|
@ -1639,8 +1654,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Palabras clave",
|
dataTermsTitle: "Palabras clave",
|
||||||
dataTermsHint: "Os termos que aparecen en máis contidos.",
|
dataTermsHint: "Os termos que aparecen en máis contidos.",
|
||||||
dataConnections: "Coincidencias ligadas",
|
dataConnections: "Coincidencias ligadas",
|
||||||
dataTopicTitle: "Todo o relacionado con",
|
|
||||||
dataTopicHint: "O que o algoritmo relaciona con este tema",
|
|
||||||
dataNoMatches: "Non hai datos cruzados que amosar.",
|
dataNoMatches: "Non hai datos cruzados que amosar.",
|
||||||
dataNoProfile: "Publica contido ou escribe o teu CV para que o algoritmo aprenda que che interesa.",
|
dataNoProfile: "Publica contido ou escribe o teu CV para que o algoritmo aprenda que che interesa.",
|
||||||
pollsDescription: "Módulo para preguntarlle á rede e contar as respostas.",
|
pollsDescription: "Módulo para preguntarlle á rede e contar as respostas.",
|
||||||
|
|
@ -2194,9 +2207,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "interacción",
|
tribeTrendingEngagement: "interacción",
|
||||||
tribeTopCategory: "Máis votado",
|
tribeTopCategory: "Máis votado",
|
||||||
tribeTagsEmpty: "Non se atoparon etiquetas.",
|
tribeTagsEmpty: "Non se atoparon etiquetas.",
|
||||||
tribeSearchPlaceholder: "Buscar contido da tribo...",
|
|
||||||
tribeSearchEmpty: "Sen resultados.",
|
tribeSearchEmpty: "Sen resultados.",
|
||||||
tribeSearchResults: "Resultados",
|
|
||||||
tribeSearchMinChars: "Introduce polo menos 2 caracteres para buscar.",
|
tribeSearchMinChars: "Introduce polo menos 2 caracteres para buscar.",
|
||||||
agendaTitle: "Axenda",
|
agendaTitle: "Axenda",
|
||||||
agendaDescription: "Aquí podes atopar todos os teus elementos asignados.",
|
agendaDescription: "Aquí podes atopar todos os teus elementos asignados.",
|
||||||
|
|
@ -3833,6 +3844,7 @@ module.exports = {
|
||||||
roomUpdate: "Actualizar",
|
roomUpdate: "Actualizar",
|
||||||
roomDelete: "Eliminar",
|
roomDelete: "Eliminar",
|
||||||
roomClose: "Pechar",
|
roomClose: "Pechar",
|
||||||
|
roomReopen: "Reabrir",
|
||||||
roomCreateSectionTitle: "Crear sala",
|
roomCreateSectionTitle: "Crear sala",
|
||||||
roomUpdateSectionTitle: "Actualizar sala",
|
roomUpdateSectionTitle: "Actualizar sala",
|
||||||
roomTitlePlaceholder: "Título da sala",
|
roomTitlePlaceholder: "Título da sala",
|
||||||
|
|
@ -3851,6 +3863,15 @@ module.exports = {
|
||||||
roomMuted: "silenciado",
|
roomMuted: "silenciado",
|
||||||
roomRec: "Gravar audio",
|
roomRec: "Gravar audio",
|
||||||
roomRecStop: "Parar a gravación",
|
roomRecStop: "Parar a gravación",
|
||||||
|
roomStreamFile: "Emitir",
|
||||||
|
roomStreamStop: "Parar",
|
||||||
|
roomStreaming: "Emitindo",
|
||||||
|
liveFullscreen: "Pantalla completa",
|
||||||
|
roomStreamPickTitle: "Escolle un vídeo",
|
||||||
|
roomStreamPickHint: "Vídeos deste dispositivo. O que escollas emítese desde onde está, sen copialo nin publicalo.",
|
||||||
|
roomStreamGo: "Emitir na sala",
|
||||||
|
roomStreamNotFound: "Ese vídeo non está nos teus cartafoles.",
|
||||||
|
roomPublicWarning: "Esta sala retransmítese na web pública: calquera pode ver e oír o que pasa nela.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4332,7 +4353,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Historial",
|
phoneHistoryTitle: "Historial",
|
||||||
phoneLatestTitle: "Últimas chamadas",
|
phoneLatestTitle: "Últimas chamadas",
|
||||||
phoneCreateButton: "Nova chamada",
|
phoneCreateButton: "Nova chamada",
|
||||||
phoneLimitsHint: "Ata 7 habitantes por chamada; con máis dun é unha chamada conxunta.",
|
phoneLimitsHint: "Ata {n} habitantes por chamada; con máis dun é unha chamada conxunta.",
|
||||||
phoneErrorPub: "Non se pode chamar a un pub.",
|
phoneErrorPub: "Non se pode chamar a un pub.",
|
||||||
phoneJointCall: "Chamada conxunta",
|
phoneJointCall: "Chamada conxunta",
|
||||||
phoneWith: "Con",
|
phoneWith: "Con",
|
||||||
|
|
@ -4477,6 +4498,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configura o nivel de exposición a mensaxes privadas que queres ter na rede.",
|
settingsPmVisibilityDesc: "Configura o nivel de exposición a mensaxes privadas que queres ter na rede.",
|
||||||
settingsPmVisibilityWhole: "Multiverso",
|
settingsPmVisibilityWhole: "Multiverso",
|
||||||
settingsPmVisibilityMutuals: "Só apoio mutuo",
|
settingsPmVisibilityMutuals: "Só apoio mutuo",
|
||||||
|
mute: "Silenciar",
|
||||||
|
unmute: "Deixar de silenciar",
|
||||||
|
relationshipMuted: "Silenciado por ti",
|
||||||
|
hideContent: "Agochar para min",
|
||||||
|
unhideContent: "Mostrar de novo",
|
||||||
|
hiddenByYou: "Agochado por ti",
|
||||||
|
settingsTabContent: "Contido",
|
||||||
|
settingsHiddenTitle: "Contido agochado",
|
||||||
|
settingsHiddenDesc: "Habitantes que silenciaches e contido que agochaches.",
|
||||||
|
settingsMutedLabel: "Habitantes silenciados",
|
||||||
|
settingsHiddenLabel: "Contido agochado",
|
||||||
|
settingsMutedEmpty: "Ninguén silenciado.",
|
||||||
|
settingsHiddenEmpty: "Nada agochado.",
|
||||||
|
settingsPmVisibilityFollowing: "Só xente á que sigo",
|
||||||
|
settingsMentionsVisibilityTitle: "Mencións",
|
||||||
|
settingsMentionsVisibilityDesc: "Escolle de quen che chegan as mencións.",
|
||||||
pmMutualNotice: "O envío é unha barreira de UX. A recepción é un filtro de atención (o cifrado xa está replicado).",
|
pmMutualNotice: "O envío é unha barreira de UX. A recepción é un filtro de atención (o cifrado xa está replicado).",
|
||||||
inhabitantsPendingFollowsTitle: "Solicitudes de apoio pendentes",
|
inhabitantsPendingFollowsTitle: "Solicitudes de apoio pendentes",
|
||||||
inhabitantsPendingAccept: "Aceptar",
|
inhabitantsPendingAccept: "Aceptar",
|
||||||
|
|
@ -4583,7 +4620,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "Taxa ECO(usuario) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "Taxa ECO(usuario) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "O imposto ECO dedúcese da RBU bruta cando se calcula a asignación da época. Os ECO deducidos non se redistribúen nesta época — quedan no saldo do pub e alimentan o fondo da próxima época a través dos límites reserveMin / alpha.",
|
bankRulesEcoTaxNote: "O imposto ECO dedúcese da RBU bruta cando se calcula a asignación da época. Os ECO deducidos non se redistribúen nesta época — quedan no saldo do pub e alimentan o fondo da próxima época a través dos límites reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation",
|
||||||
bankRulesKarmaNote: "O karma derívase das túas accións (publicacións, votos, participación no parlamento/tribunais, etc.) cun factor de decaemento temporal, menos os gramos de carbono que xera o teu feed.",
|
bankRulesKarmaNote: "O karma derívase das túas accións (publicacións, votos, participación no parlamento/tribunais, etc.) cun factor de decaemento temporal, menos os gramos de carbono que xera o teu feed. O que recibe o teu contido tamén conta: as opinións, votos, reaccións e reproducións positivas suman karma, e as opinións ou votos negativos réstano cando varios habitantes coinciden.",
|
||||||
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax",
|
||||||
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)",
|
||||||
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)",
|
||||||
|
|
@ -4618,7 +4655,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "No test WILL cada opción pondera unha ou máis casas; ao envialo, o sistema suma as puntuacións e admíteche automaticamente na casa con máis puntos.",
|
larpRulesTestEntryText: "No test WILL cada opción pondera unha ou máis casas; ao envialo, o sistema suma as puntuacións e admíteche automaticamente na casa con máis puntos.",
|
||||||
larpWillTestHint: "Unha vez completado, asignaráseche a casa que máis se axuste ás túas respostas. As túas respostas individuais procésanse localmente e nunca se almacenan — só se publica no teu feed a asignación de casa resultante.",
|
larpWillTestHint: "Unha vez completado, asignaráseche a casa que máis se axuste ás túas respostas. As túas respostas individuais procésanse localmente e nunca se almacenan — só se publica no teu feed a asignación de casa resultante.",
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Anuncia periodicamente este nodo a outras instancias de Oasis na mesma rede local. Desactívao para deter as difusións UDP.",
|
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",
|
||||||
settingsLanTitle: "Difusión LAN",
|
settingsLanTitle: "Difusión LAN",
|
||||||
settingsReplicationTitle: "Replicación",
|
settingsReplicationTitle: "Replicación",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Tor से पहुंचा जा सकता है",
|
peersTorYes: "Tor से पहुंचा जा सकता है",
|
||||||
peersTorNo: "Tor से नहीं पहुंचा जा सकता",
|
peersTorNo: "Tor से नहीं पहुंचा जा सकता",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "संस्करण",
|
peersOasisVersion: "संस्करण",
|
||||||
graphos: "ग्राफोस",
|
graphos: "ग्राफोस",
|
||||||
graphosDescription: "आपके आसपास के नेटवर्क का इंटरैक्टिव मानचित्र।",
|
graphosDescription: "आपके आसपास के नेटवर्क का इंटरैक्टिव मानचित्र।",
|
||||||
|
|
@ -602,6 +602,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "अपना संस्करण घोषित नहीं करता: पुराना Oasis",
|
peerVersionUnannounced: "अपना संस्करण घोषित नहीं करता: पुराना Oasis",
|
||||||
peersTorMissing: "यह PUB केवल TOR से कनेक्शन की अनुमति देता है।",
|
peersTorMissing: "यह PUB केवल TOR से कनेक्शन की अनुमति देता है।",
|
||||||
peerConnectionsTitle: "कनेक्शन",
|
peerConnectionsTitle: "कनेक्शन",
|
||||||
|
peersOnlineHint: "इस समय आपसे जुड़े नोड।",
|
||||||
|
peersDiscoveredHint: "ज्ञात पते वाले नोड जो अभी जुड़े नहीं हैं।",
|
||||||
|
peersUnknownHint: "आपके सामाजिक ग्राफ़ के नोड जिनका पता ज्ञात नहीं है।",
|
||||||
|
peersConnectionsHint: "वे पते जिनसे यह नोड जुड़ सकता है और हर कनेक्शन की स्थिति।",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "स्वास्थ्य",
|
||||||
|
peersHealthHint: "हर ज्ञात pub या नोड की स्थिति, स्थानीय नेटवर्क में और उसके बाहर।",
|
||||||
|
peersHealthReachable: "पहुँच योग्य",
|
||||||
|
peersHealthFailing: "विफल हो रहा",
|
||||||
|
peersHealthFailures: "विफलताएँ",
|
||||||
|
peersFailingSince: "कब से विफल",
|
||||||
|
peersHealthForget: "अप्राप्य को भूलें",
|
||||||
|
peersLanHint: "आपके स्थानीय नेटवर्क में मिले नोड।",
|
||||||
|
peersLanLastSeen: "अंतिम बार देखा गया",
|
||||||
|
peersLanEmpty: "स्थानीय नेटवर्क में अभी कोई नोड नहीं मिला।",
|
||||||
online: "ऑनलाइन",
|
online: "ऑनलाइन",
|
||||||
offline: "ऑफ़लाइन",
|
offline: "ऑफ़लाइन",
|
||||||
discovered: 'खोजे गए',
|
discovered: 'खोजे गए',
|
||||||
|
|
@ -1650,8 +1665,6 @@ module.exports = {
|
||||||
dataTermsTitle: "मुख्य शब्द",
|
dataTermsTitle: "मुख्य शब्द",
|
||||||
dataTermsHint: "वे शब्द जो सबसे अधिक सामग्री में आते हैं।",
|
dataTermsHint: "वे शब्द जो सबसे अधिक सामग्री में आते हैं।",
|
||||||
dataConnections: "जुड़े मिलान",
|
dataConnections: "जुड़े मिलान",
|
||||||
dataTopicTitle: "इससे संबंधित सब कुछ:",
|
|
||||||
dataTopicHint: "एल्गोरिद्म इस विषय से क्या जोड़ता है",
|
|
||||||
dataNoMatches: "दिखाने के लिए क्रॉस-डेटा नहीं।",
|
dataNoMatches: "दिखाने के लिए क्रॉस-डेटा नहीं।",
|
||||||
dataNoProfile: "सामग्री प्रकाशित करें या अपना CV लिखें ताकि एल्गोरिद्म जान सके कि आपकी रुचि किसमें है।",
|
dataNoProfile: "सामग्री प्रकाशित करें या अपना CV लिखें ताकि एल्गोरिद्म जान सके कि आपकी रुचि किसमें है।",
|
||||||
pollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।",
|
pollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।",
|
||||||
|
|
@ -2204,9 +2217,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "सहभागिता",
|
tribeTrendingEngagement: "सहभागिता",
|
||||||
tribeTopCategory: "अधिक मतदान",
|
tribeTopCategory: "अधिक मतदान",
|
||||||
tribeTagsEmpty: "कोई टैग नहीं मिला।",
|
tribeTagsEmpty: "कोई टैग नहीं मिला।",
|
||||||
tribeSearchPlaceholder: "जनजाति सामग्री खोजें...",
|
|
||||||
tribeSearchEmpty: "कोई परिणाम नहीं मिला।",
|
tribeSearchEmpty: "कोई परिणाम नहीं मिला।",
|
||||||
tribeSearchResults: "परिणाम",
|
|
||||||
tribeSearchMinChars: "खोजने के लिए कम से कम 2 अक्षर दर्ज करें।",
|
tribeSearchMinChars: "खोजने के लिए कम से कम 2 अक्षर दर्ज करें।",
|
||||||
agendaTitle: "एजेंडा",
|
agendaTitle: "एजेंडा",
|
||||||
agendaDescription: "यहाँ आप अपने सभी सौंपे गए आइटम पा सकते हैं।",
|
agendaDescription: "यहाँ आप अपने सभी सौंपे गए आइटम पा सकते हैं।",
|
||||||
|
|
@ -3835,6 +3846,7 @@ module.exports = {
|
||||||
roomUpdate: "अपडेट करें",
|
roomUpdate: "अपडेट करें",
|
||||||
roomDelete: "हटाएं",
|
roomDelete: "हटाएं",
|
||||||
roomClose: "बंद करें",
|
roomClose: "बंद करें",
|
||||||
|
roomReopen: "फिर से खोलें",
|
||||||
roomCreateSectionTitle: "रूम बनाएं",
|
roomCreateSectionTitle: "रूम बनाएं",
|
||||||
roomUpdateSectionTitle: "रूम अपडेट करें",
|
roomUpdateSectionTitle: "रूम अपडेट करें",
|
||||||
roomTitlePlaceholder: "रूम का शीर्षक",
|
roomTitlePlaceholder: "रूम का शीर्षक",
|
||||||
|
|
@ -3853,6 +3865,15 @@ module.exports = {
|
||||||
roomMuted: "म्यूट",
|
roomMuted: "म्यूट",
|
||||||
roomRec: "ऑडियो रिकॉर्ड करें",
|
roomRec: "ऑडियो रिकॉर्ड करें",
|
||||||
roomRecStop: "रिकॉर्डिंग रोकें",
|
roomRecStop: "रिकॉर्डिंग रोकें",
|
||||||
|
roomStreamFile: "स्ट्रीम करें",
|
||||||
|
roomStreamStop: "रोकें",
|
||||||
|
roomStreaming: "स्ट्रीम हो रहा है",
|
||||||
|
liveFullscreen: "पूर्ण स्क्रीन",
|
||||||
|
roomStreamPickTitle: "वीडियो चुनें",
|
||||||
|
roomStreamPickHint: "इस डिवाइस के वीडियो। आप जो चुनते हैं वह अपनी जगह से ही स्ट्रीम होता है, बिना कॉपी या प्रकाशित किए।",
|
||||||
|
roomStreamGo: "रूम में चलाएँ",
|
||||||
|
roomStreamNotFound: "यह वीडियो आपके फ़ोल्डरों में नहीं है।",
|
||||||
|
roomPublicWarning: "यह रूम सार्वजनिक वेब पर प्रसारित होता है: इसमें जो होता है उसे कोई भी देख और सुन सकता है।",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4333,7 +4354,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "इतिहास",
|
phoneHistoryTitle: "इतिहास",
|
||||||
phoneLatestTitle: "हाल की कॉल",
|
phoneLatestTitle: "हाल की कॉल",
|
||||||
phoneCreateButton: "नई कॉल",
|
phoneCreateButton: "नई कॉल",
|
||||||
phoneLimitsHint: "प्रति कॉल अधिकतम 7 निवासी; एक से अधिक होने पर यह संयुक्त कॉल है।",
|
phoneLimitsHint: "प्रति कॉल अधिकतम {n} निवासी; एक से अधिक होने पर यह संयुक्त कॉल है।",
|
||||||
phoneErrorPub: "पब को कॉल नहीं किया जा सकता।",
|
phoneErrorPub: "पब को कॉल नहीं किया जा सकता।",
|
||||||
phoneJointCall: "संयुक्त कॉल",
|
phoneJointCall: "संयुक्त कॉल",
|
||||||
phoneWith: "किसके साथ",
|
phoneWith: "किसके साथ",
|
||||||
|
|
@ -4478,6 +4499,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "नेटवर्क में निजी संदेश एक्सपोज़र स्तर कॉन्फ़िगर करें।",
|
settingsPmVisibilityDesc: "नेटवर्क में निजी संदेश एक्सपोज़र स्तर कॉन्फ़िगर करें।",
|
||||||
settingsPmVisibilityWhole: "मल्टीवर्स",
|
settingsPmVisibilityWhole: "मल्टीवर्स",
|
||||||
settingsPmVisibilityMutuals: "केवल पारस्परिक समर्थन",
|
settingsPmVisibilityMutuals: "केवल पारस्परिक समर्थन",
|
||||||
|
mute: "म्यूट करें",
|
||||||
|
unmute: "म्यूट हटाएँ",
|
||||||
|
relationshipMuted: "आपके द्वारा म्यूट",
|
||||||
|
hideContent: "मेरे लिए छिपाएँ",
|
||||||
|
unhideContent: "फिर से दिखाएँ",
|
||||||
|
hiddenByYou: "आपके द्वारा छिपाया गया",
|
||||||
|
settingsTabContent: "सामग्री",
|
||||||
|
settingsHiddenTitle: "छिपी हुई सामग्री",
|
||||||
|
settingsHiddenDesc: "जिन निवासियों को आपने म्यूट किया और जो सामग्री आपने छिपाई।",
|
||||||
|
settingsMutedLabel: "म्यूट किए गए निवासी",
|
||||||
|
settingsHiddenLabel: "छिपाई गई सामग्री",
|
||||||
|
settingsMutedEmpty: "कोई म्यूट नहीं।",
|
||||||
|
settingsHiddenEmpty: "कुछ भी छिपा नहीं।",
|
||||||
|
settingsPmVisibilityFollowing: "केवल वे लोग जिन्हें मैं फ़ॉलो करता हूँ",
|
||||||
|
settingsMentionsVisibilityTitle: "उल्लेख",
|
||||||
|
settingsMentionsVisibilityDesc: "चुनें कि किसके उल्लेख आप तक पहुँचें।",
|
||||||
pmMutualNotice: "आउटबाउंड UX गार्डरेल है। इनबाउंड ध्यान फ़िल्टर है।",
|
pmMutualNotice: "आउटबाउंड UX गार्डरेल है। इनबाउंड ध्यान फ़िल्टर है।",
|
||||||
inhabitantsPendingFollowsTitle: "लंबित समर्थन अनुरोध",
|
inhabitantsPendingFollowsTitle: "लंबित समर्थन अनुरोध",
|
||||||
inhabitantsPendingAccept: "स्वीकार",
|
inhabitantsPendingAccept: "स्वीकार",
|
||||||
|
|
@ -4610,7 +4647,7 @@ module.exports = {
|
||||||
bankRulesFloor: "प्रति उपयोगकर्ता न्यूनतम (सकल)",
|
bankRulesFloor: "प्रति उपयोगकर्ता न्यूनतम (सकल)",
|
||||||
bankRulesGraceDays: "दावे की अनुग्रह अवधि",
|
bankRulesGraceDays: "दावे की अनुग्रह अवधि",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "कर्म आपकी गतिविधियों (पोस्ट, वोट, संसद/अदालतों में भागीदारी आदि) से समय के साथ घटने वाले गुणक सहित निकाला जाता है, जिसमें से आपकी फ़ीड से पैदा हुए कार्बन ग्राम घटा दिए जाते हैं।",
|
bankRulesKarmaNote: "कर्म आपकी गतिविधियों (पोस्ट, वोट, संसद/अदालतों में भागीदारी आदि) से समय के साथ घटने वाले गुणक सहित निकाला जाता है, जिसमें से आपकी फ़ीड से पैदा हुए कार्बन ग्राम घटा दिए जाते हैं। आपकी सामग्री को जो मिलता है वह भी गिना जाता है: सकारात्मक राय, वोट, प्रतिक्रियाएँ और प्ले कर्म बढ़ाते हैं, और जब कई निवासी सहमत होते हैं तो नकारात्मक राय या वोट उसे घटाते हैं।",
|
||||||
bankRulesKarmaTitle: "कर्म (उपयोगकर्ता सहभागिता स्कोर)",
|
bankRulesKarmaTitle: "कर्म (उपयोगकर्ता सहभागिता स्कोर)",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
|
|
@ -4696,7 +4733,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "WILL परीक्षण में हर विकल्प एक या अधिक घरों को भार देता है; भेजने पर सिस्टम अंक जोड़ता है और सबसे अधिक अंक वाले घर में आपको स्वतः प्रवेश देता है।",
|
larpRulesTestEntryText: "WILL परीक्षण में हर विकल्प एक या अधिक घरों को भार देता है; भेजने पर सिस्टम अंक जोड़ता है और सबसे अधिक अंक वाले घर में आपको स्वतः प्रवेश देता है।",
|
||||||
larpWillTestHint: "पूरा होने पर, आपको वह गृह सौंपा जाएगा जो आपके उत्तरों के साथ सबसे अच्छा मेल खाता है। आपके व्यक्तिगत उत्तरों को स्थानीय रूप से संसाधित किया जाता है और कभी संग्रहीत नहीं किया जाता — केवल परिणामी गृह असाइनमेंट आपकी फ़ीड पर प्रकाशित होता है।",
|
larpWillTestHint: "पूरा होने पर, आपको वह गृह सौंपा जाएगा जो आपके उत्तरों के साथ सबसे अच्छा मेल खाता है। आपके व्यक्तिगत उत्तरों को स्थानीय रूप से संसाधित किया जाता है और कभी संग्रहीत नहीं किया जाता — केवल परिणामी गृह असाइनमेंट आपकी फ़ीड पर प्रकाशित होता है।",
|
||||||
larpWillTestTitle: "WILL परीक्षण",
|
larpWillTestTitle: "WILL परीक्षण",
|
||||||
settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें। UDP प्रसारण रोकने के लिए अक्षम करें।",
|
settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें।",
|
||||||
settingsLanEnable: "LAN प्रसारण सक्षम करें",
|
settingsLanEnable: "LAN प्रसारण सक्षम करें",
|
||||||
settingsLanTitle: "LAN प्रसारण",
|
settingsLanTitle: "LAN प्रसारण",
|
||||||
settingsReplicationTitle: "प्रतिकृति",
|
settingsReplicationTitle: "प्रतिकृति",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Raggiungibile tramite Tor",
|
peersTorYes: "Raggiungibile tramite Tor",
|
||||||
peersTorNo: "Non raggiungibile tramite Tor",
|
peersTorNo: "Non raggiungibile tramite Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Versione",
|
peersOasisVersion: "Versione",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Mappa interattiva della rete attorno a te.",
|
graphosDescription: "Mappa interattiva della rete attorno a te.",
|
||||||
|
|
@ -600,6 +600,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Non annuncia la sua versione: un Oasis vecchio",
|
peerVersionUnannounced: "Non annuncia la sua versione: un Oasis vecchio",
|
||||||
peersTorMissing: "Questo PUB accetta solo connessioni tramite TOR.",
|
peersTorMissing: "Questo PUB accetta solo connessioni tramite TOR.",
|
||||||
peerConnectionsTitle: "Connessioni",
|
peerConnectionsTitle: "Connessioni",
|
||||||
|
peersOnlineHint: "Nodi connessi con te in questo momento.",
|
||||||
|
peersDiscoveredHint: "Nodi con indirizzo noto che non sono connessi ora.",
|
||||||
|
peersUnknownHint: "Nodi del tuo grafo sociale senza indirizzo noto.",
|
||||||
|
peersConnectionsHint: "Indirizzi che questo nodo può contattare e lo stato di ogni connessione.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Salute",
|
||||||
|
peersHealthHint: "Come sta ogni pub o nodo conosciuto, nella rete locale e oltre.",
|
||||||
|
peersHealthReachable: "Raggiungibile",
|
||||||
|
peersHealthFailing: "In errore",
|
||||||
|
peersHealthFailures: "Errori",
|
||||||
|
peersFailingSince: "In errore da",
|
||||||
|
peersHealthForget: "Dimentica irraggiungibili",
|
||||||
|
peersLanHint: "Nodi incontrati nella tua rete locale.",
|
||||||
|
peersLanLastSeen: "Visto l'ultima volta",
|
||||||
|
peersLanEmpty: "Nessun nodo incontrato nella rete locale per ora.",
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
discovered: 'Scoperti',
|
discovered: 'Scoperti',
|
||||||
|
|
@ -1648,8 +1663,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Parole chiave",
|
dataTermsTitle: "Parole chiave",
|
||||||
dataTermsHint: "I termini che compaiono nel maggior numero di contenuti.",
|
dataTermsHint: "I termini che compaiono nel maggior numero di contenuti.",
|
||||||
dataConnections: "Corrispondenze collegate",
|
dataConnections: "Corrispondenze collegate",
|
||||||
dataTopicTitle: "Tutto ciò che riguarda",
|
|
||||||
dataTopicHint: "Ciò che l'algoritmo collega a questo tema",
|
|
||||||
dataNoMatches: "Nessun dato incrociato da mostrare.",
|
dataNoMatches: "Nessun dato incrociato da mostrare.",
|
||||||
dataNoProfile: "Pubblica contenuti o scrivi il tuo CV così l'algoritmo impara cosa ti interessa.",
|
dataNoProfile: "Pubblica contenuti o scrivi il tuo CV così l'algoritmo impara cosa ti interessa.",
|
||||||
pollsDescription: "Modulo per interrogare la rete e contare le risposte.",
|
pollsDescription: "Modulo per interrogare la rete e contare le risposte.",
|
||||||
|
|
@ -2202,9 +2215,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "coinvolgimento",
|
tribeTrendingEngagement: "coinvolgimento",
|
||||||
tribeTopCategory: "Più votato",
|
tribeTopCategory: "Più votato",
|
||||||
tribeTagsEmpty: "Nessun tag trovato.",
|
tribeTagsEmpty: "Nessun tag trovato.",
|
||||||
tribeSearchPlaceholder: "Cerca nei contenuti della tribù...",
|
|
||||||
tribeSearchEmpty: "Nessun risultato trovato.",
|
tribeSearchEmpty: "Nessun risultato trovato.",
|
||||||
tribeSearchResults: "Risultati",
|
|
||||||
tribeSearchMinChars: "Inserisci almeno 2 caratteri per cercare.",
|
tribeSearchMinChars: "Inserisci almeno 2 caratteri per cercare.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Qui trovi tutti gli elementi assegnati a te.",
|
agendaDescription: "Qui trovi tutti gli elementi assegnati a te.",
|
||||||
|
|
@ -3834,6 +3845,7 @@ module.exports = {
|
||||||
roomUpdate: "Aggiorna",
|
roomUpdate: "Aggiorna",
|
||||||
roomDelete: "Elimina",
|
roomDelete: "Elimina",
|
||||||
roomClose: "Chiudi",
|
roomClose: "Chiudi",
|
||||||
|
roomReopen: "Riapri",
|
||||||
roomCreateSectionTitle: "Crea stanza",
|
roomCreateSectionTitle: "Crea stanza",
|
||||||
roomUpdateSectionTitle: "Aggiorna stanza",
|
roomUpdateSectionTitle: "Aggiorna stanza",
|
||||||
roomTitlePlaceholder: "Titolo della stanza",
|
roomTitlePlaceholder: "Titolo della stanza",
|
||||||
|
|
@ -3852,6 +3864,15 @@ module.exports = {
|
||||||
roomMuted: "microfono disattivato",
|
roomMuted: "microfono disattivato",
|
||||||
roomRec: "Registra audio",
|
roomRec: "Registra audio",
|
||||||
roomRecStop: "Ferma la registrazione",
|
roomRecStop: "Ferma la registrazione",
|
||||||
|
roomStreamFile: "Trasmetti",
|
||||||
|
roomStreamStop: "Ferma",
|
||||||
|
roomStreaming: "In trasmissione",
|
||||||
|
liveFullscreen: "Schermo intero",
|
||||||
|
roomStreamPickTitle: "Scegli un video",
|
||||||
|
roomStreamPickHint: "Video di questo dispositivo. Quello che scegli viene trasmesso da dove si trova, senza copiarlo né pubblicarlo.",
|
||||||
|
roomStreamGo: "Trasmetti nella stanza",
|
||||||
|
roomStreamNotFound: "Quel video non è nelle tue cartelle.",
|
||||||
|
roomPublicWarning: "Questa stanza è trasmessa sul web pubblico: chiunque può vedere e sentire ciò che vi accade.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4332,7 +4353,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Cronologia",
|
phoneHistoryTitle: "Cronologia",
|
||||||
phoneLatestTitle: "Ultime chiamate",
|
phoneLatestTitle: "Ultime chiamate",
|
||||||
phoneCreateButton: "Nuova chiamata",
|
phoneCreateButton: "Nuova chiamata",
|
||||||
phoneLimitsHint: "Fino a 7 abitanti per chiamata; con più di uno è una chiamata congiunta.",
|
phoneLimitsHint: "Fino a {n} abitanti per chiamata; con più di uno è una chiamata congiunta.",
|
||||||
phoneErrorPub: "Non si può chiamare un pub.",
|
phoneErrorPub: "Non si può chiamare un pub.",
|
||||||
phoneJointCall: "Chiamata congiunta",
|
phoneJointCall: "Chiamata congiunta",
|
||||||
phoneWith: "Con",
|
phoneWith: "Con",
|
||||||
|
|
@ -4477,6 +4498,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configura il livello di esposizione ai messaggi privati.",
|
settingsPmVisibilityDesc: "Configura il livello di esposizione ai messaggi privati.",
|
||||||
settingsPmVisibilityWhole: "Multiverso",
|
settingsPmVisibilityWhole: "Multiverso",
|
||||||
settingsPmVisibilityMutuals: "Solo supporto reciproco",
|
settingsPmVisibilityMutuals: "Solo supporto reciproco",
|
||||||
|
mute: "Silenzia",
|
||||||
|
unmute: "Riattiva",
|
||||||
|
relationshipMuted: "Silenziato da te",
|
||||||
|
hideContent: "Nascondi per me",
|
||||||
|
unhideContent: "Mostra di nuovo",
|
||||||
|
hiddenByYou: "Nascosto da te",
|
||||||
|
settingsTabContent: "Contenuti",
|
||||||
|
settingsHiddenTitle: "Contenuti nascosti",
|
||||||
|
settingsHiddenDesc: "Abitanti silenziati e contenuti nascosti.",
|
||||||
|
settingsMutedLabel: "Abitanti silenziati",
|
||||||
|
settingsHiddenLabel: "Contenuti nascosti",
|
||||||
|
settingsMutedEmpty: "Nessuno silenziato.",
|
||||||
|
settingsHiddenEmpty: "Niente di nascosto.",
|
||||||
|
settingsPmVisibilityFollowing: "Solo persone che seguo",
|
||||||
|
settingsMentionsVisibilityTitle: "Menzioni",
|
||||||
|
settingsMentionsVisibilityDesc: "Scegli da chi ricevere le menzioni.",
|
||||||
pmMutualNotice: "L'invio è un guardrail UX. La ricezione è un filtro di attenzione.",
|
pmMutualNotice: "L'invio è un guardrail UX. La ricezione è un filtro di attenzione.",
|
||||||
inhabitantsPendingFollowsTitle: "Richieste di supporto in attesa",
|
inhabitantsPendingFollowsTitle: "Richieste di supporto in attesa",
|
||||||
inhabitantsPendingAccept: "Accetta",
|
inhabitantsPendingAccept: "Accetta",
|
||||||
|
|
@ -4583,7 +4620,7 @@ module.exports = {
|
||||||
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2",
|
||||||
bankRulesEcoTaxNote: "La tassa ECO viene dedotta dal RBU lordo quando si calcola l'assegnazione dell'epoca. Gli ECO dedotti non vengono ridistribuiti in questa epoca — restano nel saldo del pub e alimentano il fondo dell'epoca successiva tramite i limiti reserveMin / alpha.",
|
bankRulesEcoTaxNote: "La tassa ECO viene dedotta dal RBU lordo quando si calcola l'assegnazione dell'epoca. Gli ECO dedotti non vengono ridistribuiti in questa epoca — restano nel saldo del pub e alimentano il fondo dell'epoca successiva tramite i limiti reserveMin / alpha.",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "Il karma deriva dalle tue azioni (post, voti, partecipazione a parlamento/tribunali, ecc.) con un fattore di decadimento nel tempo, meno i grammi di carbonio generati dal tuo feed.",
|
bankRulesKarmaNote: "Il karma deriva dalle tue azioni (post, voti, partecipazione a parlamento/tribunali, ecc.) con un fattore di decadimento nel tempo, meno i grammi di carbonio generati dal tuo feed. Conta anche ciò che ricevono i tuoi contenuti: opinioni, voti, reazioni e riproduzioni positive aggiungono karma, mentre opinioni o voti negativi lo tolgono quando più abitanti concordano.",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)",
|
||||||
|
|
@ -4618,7 +4655,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "Nel test WILL ogni opzione pondera una o più case; all'invio il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.",
|
larpRulesTestEntryText: "Nel test WILL ogni opzione pondera una o più case; all'invio il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.",
|
||||||
larpWillTestHint: "Una volta completato, ti verrà assegnata la casa che meglio corrisponde alle tue risposte. Le tue risposte individuali vengono elaborate localmente e mai memorizzate — solo l'assegnazione di casa risultante viene pubblicata nel tuo feed.",
|
larpWillTestHint: "Una volta completato, ti verrà assegnata la casa che meglio corrisponde alle tue risposte. Le tue risposte individuali vengono elaborate localmente e mai memorizzate — solo l'assegnazione di casa risultante viene pubblicata nel tuo feed.",
|
||||||
larpWillTestTitle: "Test WILL",
|
larpWillTestTitle: "Test WILL",
|
||||||
settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale. Disabilita per interrompere le trasmissioni UDP.",
|
settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale.",
|
||||||
settingsLanEnable: "Abilita trasmissione LAN",
|
settingsLanEnable: "Abilita trasmissione LAN",
|
||||||
settingsLanTitle: "Trasmissione LAN",
|
settingsLanTitle: "Trasmissione LAN",
|
||||||
settingsReplicationTitle: "Replicazione",
|
settingsReplicationTitle: "Replicazione",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Acessível por Tor",
|
peersTorYes: "Acessível por Tor",
|
||||||
peersTorNo: "Não acessível por Tor",
|
peersTorNo: "Não acessível por Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Versão",
|
peersOasisVersion: "Versão",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "Mapa interativo da rede ao seu redor.",
|
graphosDescription: "Mapa interativo da rede ao seu redor.",
|
||||||
|
|
@ -600,6 +600,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Não anuncia a sua versão: um Oasis antigo",
|
peerVersionUnannounced: "Não anuncia a sua versão: um Oasis antigo",
|
||||||
peersTorMissing: "Este PUB só permite ligações através do TOR.",
|
peersTorMissing: "Este PUB só permite ligações através do TOR.",
|
||||||
peerConnectionsTitle: "Ligações",
|
peerConnectionsTitle: "Ligações",
|
||||||
|
peersOnlineHint: "Nós ligados a ti neste momento.",
|
||||||
|
peersDiscoveredHint: "Nós com endereço conhecido que não estão ligados agora.",
|
||||||
|
peersUnknownHint: "Nós do teu grafo social sem endereço conhecido.",
|
||||||
|
peersConnectionsHint: "Endereços a que este nó pode ligar e o estado de cada ligação.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Saúde",
|
||||||
|
peersHealthHint: "Como está cada pub ou nó conhecido, na rede local e fora dela.",
|
||||||
|
peersHealthReachable: "Alcançável",
|
||||||
|
peersHealthFailing: "A falhar",
|
||||||
|
peersHealthFailures: "Falhas",
|
||||||
|
peersFailingSince: "A falhar desde",
|
||||||
|
peersHealthForget: "Esquecer inalcançáveis",
|
||||||
|
peersLanHint: "Nós encontrados na tua rede local.",
|
||||||
|
peersLanLastSeen: "Visto pela última vez",
|
||||||
|
peersLanEmpty: "Ainda não foi encontrado nenhum nó na rede local.",
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
discovered: 'Descobertos',
|
discovered: 'Descobertos',
|
||||||
|
|
@ -1648,8 +1663,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Palavras-chave",
|
dataTermsTitle: "Palavras-chave",
|
||||||
dataTermsHint: "Os termos que aparecem em mais conteúdos.",
|
dataTermsHint: "Os termos que aparecem em mais conteúdos.",
|
||||||
dataConnections: "Correspondências ligadas",
|
dataConnections: "Correspondências ligadas",
|
||||||
dataTopicTitle: "Tudo o que se relaciona com",
|
|
||||||
dataTopicHint: "O que o algoritmo relaciona com este tema",
|
|
||||||
dataNoMatches: "Sem dados cruzados para mostrar.",
|
dataNoMatches: "Sem dados cruzados para mostrar.",
|
||||||
dataNoProfile: "Publica conteúdo ou escreve o teu CV para que o algoritmo aprenda o que te interessa.",
|
dataNoProfile: "Publica conteúdo ou escreve o teu CV para que o algoritmo aprenda o que te interessa.",
|
||||||
pollsDescription: "Módulo para perguntar à rede e contar as respostas.",
|
pollsDescription: "Módulo para perguntar à rede e contar as respostas.",
|
||||||
|
|
@ -2202,9 +2215,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "envolvimento",
|
tribeTrendingEngagement: "envolvimento",
|
||||||
tribeTopCategory: "Mais votado",
|
tribeTopCategory: "Mais votado",
|
||||||
tribeTagsEmpty: "Nenhum tag encontrado.",
|
tribeTagsEmpty: "Nenhum tag encontrado.",
|
||||||
tribeSearchPlaceholder: "Pesquisar conteúdo da tribo...",
|
|
||||||
tribeSearchEmpty: "Nenhum resultado encontrado.",
|
tribeSearchEmpty: "Nenhum resultado encontrado.",
|
||||||
tribeSearchResults: "Resultados",
|
|
||||||
tribeSearchMinChars: "Introduz pelo menos 2 caracteres para pesquisar.",
|
tribeSearchMinChars: "Introduz pelo menos 2 caracteres para pesquisar.",
|
||||||
agendaTitle: "Agenda",
|
agendaTitle: "Agenda",
|
||||||
agendaDescription: "Aqui podes encontrar todos os teus itens atribuídos.",
|
agendaDescription: "Aqui podes encontrar todos os teus itens atribuídos.",
|
||||||
|
|
@ -3834,6 +3845,7 @@ module.exports = {
|
||||||
roomUpdate: "Atualizar",
|
roomUpdate: "Atualizar",
|
||||||
roomDelete: "Eliminar",
|
roomDelete: "Eliminar",
|
||||||
roomClose: "Fechar",
|
roomClose: "Fechar",
|
||||||
|
roomReopen: "Reabrir",
|
||||||
roomCreateSectionTitle: "Criar sala",
|
roomCreateSectionTitle: "Criar sala",
|
||||||
roomUpdateSectionTitle: "Atualizar sala",
|
roomUpdateSectionTitle: "Atualizar sala",
|
||||||
roomTitlePlaceholder: "Título da sala",
|
roomTitlePlaceholder: "Título da sala",
|
||||||
|
|
@ -3852,6 +3864,15 @@ module.exports = {
|
||||||
roomMuted: "silenciado",
|
roomMuted: "silenciado",
|
||||||
roomRec: "Gravar áudio",
|
roomRec: "Gravar áudio",
|
||||||
roomRecStop: "Parar a gravação",
|
roomRecStop: "Parar a gravação",
|
||||||
|
roomStreamFile: "Transmitir",
|
||||||
|
roomStreamStop: "Parar",
|
||||||
|
roomStreaming: "A transmitir",
|
||||||
|
liveFullscreen: "Ecrã inteiro",
|
||||||
|
roomStreamPickTitle: "Escolhe um vídeo",
|
||||||
|
roomStreamPickHint: "Vídeos deste dispositivo. O que escolheres é transmitido a partir de onde está, sem ser copiado nem publicado.",
|
||||||
|
roomStreamGo: "Transmitir na sala",
|
||||||
|
roomStreamNotFound: "Esse vídeo não está nas tuas pastas.",
|
||||||
|
roomPublicWarning: "Esta sala é transmitida na web pública: qualquer pessoa pode ver e ouvir o que nela acontece.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4332,7 +4353,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "Histórico",
|
phoneHistoryTitle: "Histórico",
|
||||||
phoneLatestTitle: "Últimas chamadas",
|
phoneLatestTitle: "Últimas chamadas",
|
||||||
phoneCreateButton: "Nova chamada",
|
phoneCreateButton: "Nova chamada",
|
||||||
phoneLimitsHint: "Até 7 habitantes por chamada; com mais de um é uma chamada conjunta.",
|
phoneLimitsHint: "Até {n} habitantes por chamada; com mais de um é uma chamada conjunta.",
|
||||||
phoneErrorPub: "Não é possível ligar para um pub.",
|
phoneErrorPub: "Não é possível ligar para um pub.",
|
||||||
phoneJointCall: "Chamada conjunta",
|
phoneJointCall: "Chamada conjunta",
|
||||||
phoneWith: "Com",
|
phoneWith: "Com",
|
||||||
|
|
@ -4477,6 +4498,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Configure seu nível de exposição a mensagens privadas.",
|
settingsPmVisibilityDesc: "Configure seu nível de exposição a mensagens privadas.",
|
||||||
settingsPmVisibilityWhole: "Multiverso",
|
settingsPmVisibilityWhole: "Multiverso",
|
||||||
settingsPmVisibilityMutuals: "Só apoio mútuo",
|
settingsPmVisibilityMutuals: "Só apoio mútuo",
|
||||||
|
mute: "Silenciar",
|
||||||
|
unmute: "Deixar de silenciar",
|
||||||
|
relationshipMuted: "Silenciado por ti",
|
||||||
|
hideContent: "Ocultar para mim",
|
||||||
|
unhideContent: "Mostrar de novo",
|
||||||
|
hiddenByYou: "Ocultado por ti",
|
||||||
|
settingsTabContent: "Conteúdo",
|
||||||
|
settingsHiddenTitle: "Conteúdo oculto",
|
||||||
|
settingsHiddenDesc: "Habitantes que silenciaste e conteúdo que ocultaste.",
|
||||||
|
settingsMutedLabel: "Habitantes silenciados",
|
||||||
|
settingsHiddenLabel: "Conteúdo oculto",
|
||||||
|
settingsMutedEmpty: "Ninguém silenciado.",
|
||||||
|
settingsHiddenEmpty: "Nada oculto.",
|
||||||
|
settingsPmVisibilityFollowing: "Só pessoas que sigo",
|
||||||
|
settingsMentionsVisibilityTitle: "Menções",
|
||||||
|
settingsMentionsVisibilityDesc: "Escolhe de quem te chegam as menções.",
|
||||||
pmMutualNotice: "O envio é uma barreira de UX. O recebimento é um filtro de atenção.",
|
pmMutualNotice: "O envio é uma barreira de UX. O recebimento é um filtro de atenção.",
|
||||||
inhabitantsPendingFollowsTitle: "Pedidos de apoio pendentes",
|
inhabitantsPendingFollowsTitle: "Pedidos de apoio pendentes",
|
||||||
inhabitantsPendingAccept: "Aceitar",
|
inhabitantsPendingAccept: "Aceitar",
|
||||||
|
|
@ -4595,7 +4632,7 @@ module.exports = {
|
||||||
bankRulesFloor: "Mínimo por utilizador (bruto)",
|
bankRulesFloor: "Mínimo por utilizador (bruto)",
|
||||||
bankRulesGraceDays: "Período de carência da reivindicação",
|
bankRulesGraceDays: "Período de carência da reivindicação",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "O karma deriva das tuas ações (publicações, votos, participação no parlamento/tribunais, etc.) com um fator de decaimento temporal, menos os gramas de carbono gerados pelo teu feed.",
|
bankRulesKarmaNote: "O karma deriva das tuas ações (publicações, votos, participação no parlamento/tribunais, etc.) com um fator de decaimento temporal, menos os gramas de carbono gerados pelo teu feed. O que o teu conteúdo recebe também conta: opiniões, votos, reações e reproduções positivas somam karma, e opiniões ou votos negativos subtraem-no quando vários habitantes coincidem.",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
bankRulesReserveMin: "Reserva mínima (mantida no saldo do pub)",
|
bankRulesReserveMin: "Reserva mínima (mantida no saldo do pub)",
|
||||||
|
|
@ -4633,7 +4670,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "No teste WILL cada opção pondera uma ou mais casas; ao enviar, o sistema soma as pontuações e admite-te automaticamente na casa com mais pontos.",
|
larpRulesTestEntryText: "No teste WILL cada opção pondera uma ou mais casas; ao enviar, o sistema soma as pontuações e admite-te automaticamente na casa com mais pontos.",
|
||||||
larpWillTestHint: "Uma vez concluído, ser-te-á atribuída a casa que melhor corresponde às tuas respostas. As tuas respostas individuais são processadas localmente e nunca armazenadas — apenas a atribuição de casa resultante é publicada no teu feed.",
|
larpWillTestHint: "Uma vez concluído, ser-te-á atribuída a casa que melhor corresponde às tuas respostas. As tuas respostas individuais são processadas localmente e nunca armazenadas — apenas a atribuição de casa resultante é publicada no teu feed.",
|
||||||
larpWillTestTitle: "Teste WILL",
|
larpWillTestTitle: "Teste WILL",
|
||||||
settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local. Desativar para parar as difusões UDP.",
|
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",
|
||||||
settingsLanTitle: "Difusão LAN",
|
settingsLanTitle: "Difusão LAN",
|
||||||
settingsReplicationTitle: "Replicação",
|
settingsReplicationTitle: "Replicação",
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "Доступен через Tor",
|
peersTorYes: "Доступен через Tor",
|
||||||
peersTorNo: "Недоступен через Tor",
|
peersTorNo: "Недоступен через Tor",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "Версия",
|
peersOasisVersion: "Версия",
|
||||||
graphos: "Графос",
|
graphos: "Графос",
|
||||||
graphosDescription: "Интерактивная карта сети вокруг вас.",
|
graphosDescription: "Интерактивная карта сети вокруг вас.",
|
||||||
|
|
@ -611,6 +611,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "Не сообщает свою версию: старый Oasis",
|
peerVersionUnannounced: "Не сообщает свою версию: старый Oasis",
|
||||||
peersTorMissing: "Этот PUB принимает подключения только через TOR.",
|
peersTorMissing: "Этот PUB принимает подключения только через TOR.",
|
||||||
peerConnectionsTitle: "Подключения",
|
peerConnectionsTitle: "Подключения",
|
||||||
|
peersOnlineHint: "Узлы, подключённые к вам прямо сейчас.",
|
||||||
|
peersDiscoveredHint: "Узлы с известным адресом, не подключённые сейчас.",
|
||||||
|
peersUnknownHint: "Узлы вашего социального графа без известного адреса.",
|
||||||
|
peersConnectionsHint: "Адреса, к которым может подключаться этот узел, и состояние каждого соединения.",
|
||||||
|
peersTabLan: "LAN",
|
||||||
|
peersTabHealth: "Состояние",
|
||||||
|
peersHealthHint: "Как дела у каждого известного паба или узла, в локальной сети и за её пределами.",
|
||||||
|
peersHealthReachable: "Доступен",
|
||||||
|
peersHealthFailing: "Сбоит",
|
||||||
|
peersHealthFailures: "Сбои",
|
||||||
|
peersFailingSince: "Сбоит с",
|
||||||
|
peersHealthForget: "Забыть недоступные",
|
||||||
|
peersLanHint: "Узлы, найденные в вашей локальной сети.",
|
||||||
|
peersLanLastSeen: "Последний раз замечен",
|
||||||
|
peersLanEmpty: "В локальной сети пока не найдено ни одного узла.",
|
||||||
online: "В сети",
|
online: "В сети",
|
||||||
offline: "Не в сети",
|
offline: "Не в сети",
|
||||||
discovered: 'Обнаружен',
|
discovered: 'Обнаружен',
|
||||||
|
|
@ -1658,8 +1673,6 @@ module.exports = {
|
||||||
dataTermsTitle: "Ключевые слова",
|
dataTermsTitle: "Ключевые слова",
|
||||||
dataTermsHint: "Термины, встречающиеся в наибольшем числе материалов.",
|
dataTermsHint: "Термины, встречающиеся в наибольшем числе материалов.",
|
||||||
dataConnections: "Связанные совпадения",
|
dataConnections: "Связанные совпадения",
|
||||||
dataTopicTitle: "Всё, что связано с",
|
|
||||||
dataTopicHint: "Что алгоритм связывает с этой темой",
|
|
||||||
dataNoMatches: "Нет перекрёстных данных.",
|
dataNoMatches: "Нет перекрёстных данных.",
|
||||||
dataNoProfile: "Публикуйте контент или заполните CV, чтобы алгоритм узнал, что вам интересно.",
|
dataNoProfile: "Публикуйте контент или заполните CV, чтобы алгоритм узнал, что вам интересно.",
|
||||||
pollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.",
|
pollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.",
|
||||||
|
|
@ -2218,9 +2231,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "вовлечённость",
|
tribeTrendingEngagement: "вовлечённость",
|
||||||
tribeTopCategory: "Больше голосов",
|
tribeTopCategory: "Больше голосов",
|
||||||
tribeTagsEmpty: "Тегов не найдено.",
|
tribeTagsEmpty: "Тегов не найдено.",
|
||||||
tribeSearchPlaceholder: "Поиск контента племени...",
|
|
||||||
tribeSearchEmpty: "Результаты не найдены.",
|
tribeSearchEmpty: "Результаты не найдены.",
|
||||||
tribeSearchResults: "Результаты",
|
|
||||||
tribeSearchMinChars: "Введите не менее 2 символов для поиска.",
|
tribeSearchMinChars: "Введите не менее 2 символов для поиска.",
|
||||||
agendaTitle: "Повестка дня",
|
agendaTitle: "Повестка дня",
|
||||||
agendaDescription: "Здесь вы можете найти все назначенные вам элементы.",
|
agendaDescription: "Здесь вы можете найти все назначенные вам элементы.",
|
||||||
|
|
@ -3835,6 +3846,7 @@ module.exports = {
|
||||||
roomUpdate: "Обновить",
|
roomUpdate: "Обновить",
|
||||||
roomDelete: "Удалить",
|
roomDelete: "Удалить",
|
||||||
roomClose: "Закрыть",
|
roomClose: "Закрыть",
|
||||||
|
roomReopen: "Открыть снова",
|
||||||
roomCreateSectionTitle: "Создать комнату",
|
roomCreateSectionTitle: "Создать комнату",
|
||||||
roomUpdateSectionTitle: "Обновить комнату",
|
roomUpdateSectionTitle: "Обновить комнату",
|
||||||
roomTitlePlaceholder: "Название комнаты",
|
roomTitlePlaceholder: "Название комнаты",
|
||||||
|
|
@ -3853,6 +3865,15 @@ module.exports = {
|
||||||
roomMuted: "микрофон выключен",
|
roomMuted: "микрофон выключен",
|
||||||
roomRec: "Записать аудио",
|
roomRec: "Записать аудио",
|
||||||
roomRecStop: "Остановить запись",
|
roomRecStop: "Остановить запись",
|
||||||
|
roomStreamFile: "Транслировать",
|
||||||
|
roomStreamStop: "Остановить",
|
||||||
|
roomStreaming: "Идёт трансляция",
|
||||||
|
liveFullscreen: "Во весь экран",
|
||||||
|
roomStreamPickTitle: "Выберите видео",
|
||||||
|
roomStreamPickHint: "Видео на этом устройстве. Выбранное транслируется прямо с места хранения, без копирования и публикации.",
|
||||||
|
roomStreamGo: "Показать в комнате",
|
||||||
|
roomStreamNotFound: "Этого видео нет в ваших папках.",
|
||||||
|
roomPublicWarning: "Эта комната транслируется в открытом интернете: любой может видеть и слышать, что в ней происходит.",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4333,7 +4354,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "История",
|
phoneHistoryTitle: "История",
|
||||||
phoneLatestTitle: "Последние звонки",
|
phoneLatestTitle: "Последние звонки",
|
||||||
phoneCreateButton: "Новый звонок",
|
phoneCreateButton: "Новый звонок",
|
||||||
phoneLimitsHint: "До 7 жителей на звонок; если больше одного — это совместный звонок.",
|
phoneLimitsHint: "До {n} жителей на звонок; если больше одного — это совместный звонок.",
|
||||||
phoneErrorPub: "Пабам звонить нельзя.",
|
phoneErrorPub: "Пабам звонить нельзя.",
|
||||||
phoneJointCall: "Совместный звонок",
|
phoneJointCall: "Совместный звонок",
|
||||||
phoneWith: "С кем",
|
phoneWith: "С кем",
|
||||||
|
|
@ -4478,6 +4499,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "Настройте уровень открытости личных сообщений.",
|
settingsPmVisibilityDesc: "Настройте уровень открытости личных сообщений.",
|
||||||
settingsPmVisibilityWhole: "Мультивселенная",
|
settingsPmVisibilityWhole: "Мультивселенная",
|
||||||
settingsPmVisibilityMutuals: "Только взаимная поддержка",
|
settingsPmVisibilityMutuals: "Только взаимная поддержка",
|
||||||
|
mute: "Заглушить",
|
||||||
|
unmute: "Снять заглушение",
|
||||||
|
relationshipMuted: "Заглушён вами",
|
||||||
|
hideContent: "Скрыть для меня",
|
||||||
|
unhideContent: "Показать снова",
|
||||||
|
hiddenByYou: "Скрыто вами",
|
||||||
|
settingsTabContent: "Контент",
|
||||||
|
settingsHiddenTitle: "Скрытый контент",
|
||||||
|
settingsHiddenDesc: "Заглушённые вами жители и скрытый вами контент.",
|
||||||
|
settingsMutedLabel: "Заглушённые жители",
|
||||||
|
settingsHiddenLabel: "Скрытый контент",
|
||||||
|
settingsMutedEmpty: "Никто не заглушён.",
|
||||||
|
settingsHiddenEmpty: "Ничего не скрыто.",
|
||||||
|
settingsPmVisibilityFollowing: "Только те, на кого я подписан",
|
||||||
|
settingsMentionsVisibilityTitle: "Упоминания",
|
||||||
|
settingsMentionsVisibilityDesc: "Выберите, чьи упоминания доходят до вас.",
|
||||||
pmMutualNotice: "Исходящие — защита UX. Входящие — фильтр внимания.",
|
pmMutualNotice: "Исходящие — защита UX. Входящие — фильтр внимания.",
|
||||||
inhabitantsPendingFollowsTitle: "Ожидающие запросы на поддержку",
|
inhabitantsPendingFollowsTitle: "Ожидающие запросы на поддержку",
|
||||||
inhabitantsPendingAccept: "Принять",
|
inhabitantsPendingAccept: "Принять",
|
||||||
|
|
@ -4610,7 +4647,7 @@ module.exports = {
|
||||||
bankRulesFloor: "Минимум на пользователя (брутто)",
|
bankRulesFloor: "Минимум на пользователя (брутто)",
|
||||||
bankRulesGraceDays: "Льготный период для заявки",
|
bankRulesGraceDays: "Льготный период для заявки",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "Карма вычисляется по вашим действиям (публикации, голоса, участие в парламенте/судах и т. д.) с коэффициентом затухания во времени, за вычетом граммов углерода, созданных вашей лентой.",
|
bankRulesKarmaNote: "Карма вычисляется по вашим действиям (публикации, голоса, участие в парламенте/судах и т. д.) с коэффициентом затухания во времени, за вычетом граммов углерода, созданных вашей лентой. То, что получает ваш контент, тоже учитывается: положительные мнения, голоса, реакции и прослушивания добавляют карму, а отрицательные мнения или голоса отнимают её, когда несколько жителей согласны.",
|
||||||
bankRulesKarmaTitle: "Карма (оценка вовлечённости)",
|
bankRulesKarmaTitle: "Карма (оценка вовлечённости)",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
|
|
@ -4696,7 +4733,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "В тесте WILL каждый вариант даёт вес одному или нескольким домам; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим счётом.",
|
larpRulesTestEntryText: "В тесте WILL каждый вариант даёт вес одному или нескольким домам; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим счётом.",
|
||||||
larpWillTestHint: "После завершения вам будет назначен дом, который лучше всего соответствует вашим ответам. Ваши индивидуальные ответы обрабатываются локально и никогда не сохраняются — в ваш канал публикуется только результат назначения дома.",
|
larpWillTestHint: "После завершения вам будет назначен дом, который лучше всего соответствует вашим ответам. Ваши индивидуальные ответы обрабатываются локально и никогда не сохраняются — в ваш канал публикуется только результат назначения дома.",
|
||||||
larpWillTestTitle: "WILL-тест",
|
larpWillTestTitle: "WILL-тест",
|
||||||
settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети. Отключите, чтобы остановить UDP-вещание.",
|
settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети.",
|
||||||
settingsLanEnable: "Включить LAN-вещание",
|
settingsLanEnable: "Включить LAN-вещание",
|
||||||
settingsLanTitle: "LAN-вещание",
|
settingsLanTitle: "LAN-вещание",
|
||||||
settingsReplicationTitle: "Репликация",
|
settingsReplicationTitle: "Репликация",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ module.exports = {
|
||||||
peersTorColumn: "TOR",
|
peersTorColumn: "TOR",
|
||||||
peersTorYes: "可通过 Tor 访问",
|
peersTorYes: "可通过 Tor 访问",
|
||||||
peersTorNo: "无法通过 Tor 访问",
|
peersTorNo: "无法通过 Tor 访问",
|
||||||
peersOasisId: "Oasis ID",
|
peersOasisId: "ID",
|
||||||
peersOasisVersion: "版本",
|
peersOasisVersion: "版本",
|
||||||
graphos: "Graphos",
|
graphos: "Graphos",
|
||||||
graphosDescription: "你周围网络的互动地图。",
|
graphosDescription: "你周围网络的互动地图。",
|
||||||
|
|
@ -602,6 +602,21 @@ module.exports = {
|
||||||
peerVersionUnannounced: "未公布其版本:旧版 Oasis",
|
peerVersionUnannounced: "未公布其版本:旧版 Oasis",
|
||||||
peersTorMissing: "此 PUB 仅允许通过 TOR 连接。",
|
peersTorMissing: "此 PUB 仅允许通过 TOR 连接。",
|
||||||
peerConnectionsTitle: "连接",
|
peerConnectionsTitle: "连接",
|
||||||
|
peersOnlineHint: "当前与你相连的节点。",
|
||||||
|
peersDiscoveredHint: "地址已知但当前未连接的节点。",
|
||||||
|
peersUnknownHint: "你的社交图谱中没有已知地址的节点。",
|
||||||
|
peersConnectionsHint: "本节点可以拨号的地址及每个连接的状态。",
|
||||||
|
peersTabLan: "局域网",
|
||||||
|
peersTabHealth: "健康",
|
||||||
|
peersHealthHint: "每个已知 pub 或节点的状态,包括局域网内外。",
|
||||||
|
peersHealthReachable: "可达",
|
||||||
|
peersHealthFailing: "失败中",
|
||||||
|
peersHealthFailures: "失败次数",
|
||||||
|
peersFailingSince: "失败开始于",
|
||||||
|
peersHealthForget: "忘记不可达节点",
|
||||||
|
peersLanHint: "在你的局域网中发现的节点。",
|
||||||
|
peersLanLastSeen: "最后出现",
|
||||||
|
peersLanEmpty: "局域网中尚未发现任何节点。",
|
||||||
online: "在线",
|
online: "在线",
|
||||||
offline: "离线",
|
offline: "离线",
|
||||||
discovered: '已发现',
|
discovered: '已发现',
|
||||||
|
|
@ -1651,8 +1666,6 @@ module.exports = {
|
||||||
dataTermsTitle: "关键词",
|
dataTermsTitle: "关键词",
|
||||||
dataTermsHint: "出现在最多内容中的词条。",
|
dataTermsHint: "出现在最多内容中的词条。",
|
||||||
dataConnections: "关联匹配",
|
dataConnections: "关联匹配",
|
||||||
dataTopicTitle: "与此相关的一切:",
|
|
||||||
dataTopicHint: "算法与该主题关联的内容",
|
|
||||||
dataNoMatches: "没有可显示的交叉数据。",
|
dataNoMatches: "没有可显示的交叉数据。",
|
||||||
dataNoProfile: "发布内容或填写简历,让算法了解你的兴趣。",
|
dataNoProfile: "发布内容或填写简历,让算法了解你的兴趣。",
|
||||||
pollsDescription: "用于向网络提问并统计答案的模块。",
|
pollsDescription: "用于向网络提问并统计答案的模块。",
|
||||||
|
|
@ -2205,9 +2218,7 @@ module.exports = {
|
||||||
tribeTrendingEngagement: "参与度",
|
tribeTrendingEngagement: "参与度",
|
||||||
tribeTopCategory: "最多投票",
|
tribeTopCategory: "最多投票",
|
||||||
tribeTagsEmpty: "未找到标签。",
|
tribeTagsEmpty: "未找到标签。",
|
||||||
tribeSearchPlaceholder: "搜索部落内容...",
|
|
||||||
tribeSearchEmpty: "未找到结果。",
|
tribeSearchEmpty: "未找到结果。",
|
||||||
tribeSearchResults: "结果",
|
|
||||||
tribeSearchMinChars: "请输入至少2个字符进行搜索。",
|
tribeSearchMinChars: "请输入至少2个字符进行搜索。",
|
||||||
agendaTitle: "日程",
|
agendaTitle: "日程",
|
||||||
agendaDescription: "在这里你可以找到所有分配给你的项目。",
|
agendaDescription: "在这里你可以找到所有分配给你的项目。",
|
||||||
|
|
@ -3836,6 +3847,7 @@ module.exports = {
|
||||||
roomUpdate: "更新",
|
roomUpdate: "更新",
|
||||||
roomDelete: "删除",
|
roomDelete: "删除",
|
||||||
roomClose: "关闭",
|
roomClose: "关闭",
|
||||||
|
roomReopen: "重新开放",
|
||||||
roomCreateSectionTitle: "创建房间",
|
roomCreateSectionTitle: "创建房间",
|
||||||
roomUpdateSectionTitle: "更新房间",
|
roomUpdateSectionTitle: "更新房间",
|
||||||
roomTitlePlaceholder: "房间标题",
|
roomTitlePlaceholder: "房间标题",
|
||||||
|
|
@ -3854,6 +3866,15 @@ module.exports = {
|
||||||
roomMuted: "已静音",
|
roomMuted: "已静音",
|
||||||
roomRec: "录制音频",
|
roomRec: "录制音频",
|
||||||
roomRecStop: "停止录音",
|
roomRecStop: "停止录音",
|
||||||
|
roomStreamFile: "播放",
|
||||||
|
roomStreamStop: "停止",
|
||||||
|
roomStreaming: "正在播放",
|
||||||
|
liveFullscreen: "全屏",
|
||||||
|
roomStreamPickTitle: "选择视频",
|
||||||
|
roomStreamPickHint: "此设备上的视频。所选视频将从其所在位置直接播放,不会被复制或发布。",
|
||||||
|
roomStreamGo: "在房间中播放",
|
||||||
|
roomStreamNotFound: "你的文件夹中没有这个视频。",
|
||||||
|
roomPublicWarning: "此房间在公共网络上直播:任何人都能看到和听到其中发生的一切。",
|
||||||
roomRecording: "REC",
|
roomRecording: "REC",
|
||||||
roomNotificationsOn: "ON",
|
roomNotificationsOn: "ON",
|
||||||
roomNotificationsOff: "OFF",
|
roomNotificationsOff: "OFF",
|
||||||
|
|
@ -4334,7 +4355,7 @@ module.exports = {
|
||||||
phoneHistoryTitle: "历史记录",
|
phoneHistoryTitle: "历史记录",
|
||||||
phoneLatestTitle: "最近通话",
|
phoneLatestTitle: "最近通话",
|
||||||
phoneCreateButton: "新通话",
|
phoneCreateButton: "新通话",
|
||||||
phoneLimitsHint: "每次通话最多 7 位居民;多于一位即为联合通话。",
|
phoneLimitsHint: "每次通话最多 {n} 位居民;多于一位即为联合通话。",
|
||||||
phoneErrorPub: "不能呼叫 pub。",
|
phoneErrorPub: "不能呼叫 pub。",
|
||||||
phoneJointCall: "联合通话",
|
phoneJointCall: "联合通话",
|
||||||
phoneWith: "对象",
|
phoneWith: "对象",
|
||||||
|
|
@ -4479,6 +4500,22 @@ module.exports = {
|
||||||
settingsPmVisibilityDesc: "配置您希望在网络中的私信曝光级别。",
|
settingsPmVisibilityDesc: "配置您希望在网络中的私信曝光级别。",
|
||||||
settingsPmVisibilityWhole: "多元宇宙",
|
settingsPmVisibilityWhole: "多元宇宙",
|
||||||
settingsPmVisibilityMutuals: "仅限互相支持",
|
settingsPmVisibilityMutuals: "仅限互相支持",
|
||||||
|
mute: "静音",
|
||||||
|
unmute: "取消静音",
|
||||||
|
relationshipMuted: "已被你静音",
|
||||||
|
hideContent: "对我隐藏",
|
||||||
|
unhideContent: "重新显示",
|
||||||
|
hiddenByYou: "已被你隐藏",
|
||||||
|
settingsTabContent: "内容",
|
||||||
|
settingsHiddenTitle: "隐藏的内容",
|
||||||
|
settingsHiddenDesc: "你静音的居民和你隐藏的内容。",
|
||||||
|
settingsMutedLabel: "已静音的居民",
|
||||||
|
settingsHiddenLabel: "已隐藏的内容",
|
||||||
|
settingsMutedEmpty: "没有静音任何人。",
|
||||||
|
settingsHiddenEmpty: "没有隐藏任何内容。",
|
||||||
|
settingsPmVisibilityFollowing: "仅限我关注的人",
|
||||||
|
settingsMentionsVisibilityTitle: "提及",
|
||||||
|
settingsMentionsVisibilityDesc: "选择谁的提及可以送达你。",
|
||||||
pmMutualNotice: "出站是 UX 保护。入站是关注过滤器。",
|
pmMutualNotice: "出站是 UX 保护。入站是关注过滤器。",
|
||||||
inhabitantsPendingFollowsTitle: "待处理的支持请求",
|
inhabitantsPendingFollowsTitle: "待处理的支持请求",
|
||||||
inhabitantsPendingAccept: "接受",
|
inhabitantsPendingAccept: "接受",
|
||||||
|
|
@ -4611,7 +4648,7 @@ module.exports = {
|
||||||
bankRulesFloor: "每位用户的保底(总额)",
|
bankRulesFloor: "每位用户的保底(总额)",
|
||||||
bankRulesGraceDays: "领取宽限期",
|
bankRulesGraceDays: "领取宽限期",
|
||||||
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))",
|
||||||
bankRulesKarmaNote: "业力来自你的行为(发帖、投票、参与议会/法庭等),带有随时间衰减的系数,并减去你的动态所产生的碳克数。",
|
bankRulesKarmaNote: "业力来自你的行为(发帖、投票、参与议会/法庭等),带有随时间衰减的系数,并减去你的动态所产生的碳克数。 你的内容收到的反馈同样计入:正面的评价、投票、反应和播放会增加业力,当多位居民一致给出负面评价或投票时则会扣减。",
|
||||||
bankRulesKarmaTitle: "业力(用户参与度评分)",
|
bankRulesKarmaTitle: "业力(用户参与度评分)",
|
||||||
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)",
|
||||||
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)",
|
||||||
|
|
@ -4697,7 +4734,7 @@ module.exports = {
|
||||||
larpRulesTestEntryText: "在 WILL 测试中,每个选项会为一个或多个家族加权;提交后系统统计得分,并自动把你录入得分最高的家族。",
|
larpRulesTestEntryText: "在 WILL 测试中,每个选项会为一个或多个家族加权;提交后系统统计得分,并自动把你录入得分最高的家族。",
|
||||||
larpWillTestHint: "完成后,你将被分配到与你的回答最匹配的家族。你的单个回答在本地处理,从不存储 — 只有最终的家族分配会发布到你的信息流。",
|
larpWillTestHint: "完成后,你将被分配到与你的回答最匹配的家族。你的单个回答在本地处理,从不存储 — 只有最终的家族分配会发布到你的信息流。",
|
||||||
larpWillTestTitle: "WILL 测试",
|
larpWillTestTitle: "WILL 测试",
|
||||||
settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。禁用以停止 UDP 广播。",
|
settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。",
|
||||||
settingsLanEnable: "启用局域网广播",
|
settingsLanEnable: "启用局域网广播",
|
||||||
settingsLanTitle: "局域网广播",
|
settingsLanTitle: "局域网广播",
|
||||||
settingsReplicationTitle: "复制",
|
settingsReplicationTitle: "复制",
|
||||||
|
|
|
||||||
|
|
@ -161,13 +161,14 @@ 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';
|
||||||
if (cfg.pmVisibility !== 'whole' && cfg.pmVisibility !== 'mutuals') cfg.pmVisibility = 'whole';
|
if (!['whole', 'mutuals', 'following'].includes(cfg.pmVisibility)) cfg.pmVisibility = '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 = {};
|
||||||
cfg.phone = { visibility: cfg.phone.visibility === 'whole' ? 'whole' : 'mutuals', dnd: cfg.phone.dnd === true, relay: cfg.phone.relay !== false, video: cfg.phone.video !== false, ...(typeof cfg.phone.videoDevice === 'string' && cfg.phone.videoDevice.trim() ? { videoDevice: cfg.phone.videoDevice.trim() } : {}) };
|
cfg.phone = { visibility: cfg.phone.visibility === 'whole' ? 'whole' : 'mutuals', dnd: cfg.phone.dnd === true, relay: cfg.phone.relay !== false, video: cfg.phone.video !== false, ...(typeof cfg.phone.videoDevice === 'string' && cfg.phone.videoDevice.trim() ? { videoDevice: cfg.phone.videoDevice.trim() } : {}) };
|
||||||
if (typeof cfg.ux === 'string') cfg.ux = { current: cfg.ux };
|
if (typeof cfg.ux === 'string') cfg.ux = { current: cfg.ux };
|
||||||
if (!cfg.ux || typeof cfg.ux !== 'object') cfg.ux = { current: 'blocks' };
|
if (!cfg.ux || typeof cfg.ux !== 'object') cfg.ux = { current: 'blocks' };
|
||||||
if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks';
|
if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks';
|
||||||
if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav' && cfg.ux.current !== 'chats' && cfg.ux.current !== 'feed' && cfg.ux.current !== 'phone') cfg.ux.current = 'blocks';
|
if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav' && cfg.ux.current !== 'chats' && cfg.ux.current !== 'feed' && cfg.ux.current !== 'phone' && cfg.ux.current !== 'rooms') cfg.ux.current = 'blocks';
|
||||||
if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks';
|
if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks';
|
||||||
if (cfg.ux.current === 'chats' && cfg.modules && cfg.modules.chatsMod !== 'on') cfg.ux.current = 'blocks';
|
if (cfg.ux.current === 'chats' && cfg.modules && cfg.modules.chatsMod !== 'on') cfg.ux.current = 'blocks';
|
||||||
|
|
||||||
|
|
@ -182,6 +183,7 @@ const readConfig = () => {
|
||||||
delete cfg.bottomBarPin;
|
delete cfg.bottomBarPin;
|
||||||
if (cfg.ux.current === 'phone' && cfg.modules && cfg.modules.phoneMod === 'off') cfg.ux.current = 'blocks';
|
if (cfg.ux.current === 'phone' && cfg.modules && cfg.modules.phoneMod === 'off') cfg.ux.current = 'blocks';
|
||||||
if (cfg.ux.current === 'feed' && cfg.modules && cfg.modules.feedMod === 'off') cfg.ux.current = 'blocks';
|
if (cfg.ux.current === 'feed' && cfg.modules && cfg.modules.feedMod === 'off') cfg.ux.current = 'blocks';
|
||||||
|
if (cfg.ux.current === 'rooms' && cfg.modules && cfg.modules.roomsMod === 'off') cfg.ux.current = 'blocks';
|
||||||
if (!cfg.blobCache || typeof cfg.blobCache !== 'object' || !Number.isFinite(Number(cfg.blobCache.maxMB))) cfg.blobCache = { maxMB: 0 };
|
if (!cfg.blobCache || typeof cfg.blobCache !== 'object' || !Number.isFinite(Number(cfg.blobCache.maxMB))) cfg.blobCache = { maxMB: 0 };
|
||||||
if (!Number.isFinite(Number(cfg.blobCache.pubMaxMB))) cfg.blobCache.pubMaxMB = 0;
|
if (!Number.isFinite(Number(cfg.blobCache.pubMaxMB))) cfg.blobCache.pubMaxMB = 0;
|
||||||
if (cfg.modules && typeof cfg.modules === 'object') {
|
if (cfg.modules && typeof cfg.modules === 'object') {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const FOLDERS = {
|
||||||
'content_favorites.json': 'content',
|
'content_favorites.json': 'content',
|
||||||
'blob-access.json': 'content',
|
'blob-access.json': 'content',
|
||||||
'follow_state.json': 'content',
|
'follow_state.json': 'content',
|
||||||
|
'viewer_hidden.json': 'content',
|
||||||
'invites-used.json': 'content',
|
'invites-used.json': 'content',
|
||||||
'agenda-config.json': 'content',
|
'agenda-config.json': 'content',
|
||||||
'AI-history.json': 'ai',
|
'AI-history.json': 'ai',
|
||||||
|
|
|
||||||
|
|
@ -432,10 +432,6 @@ const UBI_MAX_ATTEMPTS = 6;
|
||||||
const UBI_RETRY_AFTER_MS = 60 * 60 * 1000;
|
const UBI_RETRY_AFTER_MS = 60 * 60 * 1000;
|
||||||
const isUbiPayoutConcept = (c) => UBI_PAYMENT_PREFIXES.some(p => String(c || "").startsWith(p));
|
const isUbiPayoutConcept = (c) => UBI_PAYMENT_PREFIXES.some(p => String(c || "").startsWith(p));
|
||||||
const TXID_RE = /\b([0-9a-f]{64})\b/i;
|
const TXID_RE = /\b([0-9a-f]{64})\b/i;
|
||||||
const OWN_SCORE_CAP = 200;
|
|
||||||
const RECEIVED_AUTHOR_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
const NEGATIVE_OPINIONS = new Set(["boring", "spam", "propaganda"]);
|
|
||||||
const ACTIONS_CACHE_TTL_MS = 60 * 1000;
|
|
||||||
|
|
||||||
function resolveUserId(maybeId) {
|
function resolveUserId(maybeId) {
|
||||||
const s = String(maybeId || "").trim();
|
const s = String(maybeId || "").trim();
|
||||||
|
|
@ -443,7 +439,7 @@ function resolveUserId(maybeId) {
|
||||||
return config?.keys?.id || "";
|
return config?.keys?.id || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
let FEED_SRC = "none";
|
|
||||||
|
|
||||||
module.exports = ({ services } = {}) => {
|
module.exports = ({ services } = {}) => {
|
||||||
const transfersRepo = {
|
const transfersRepo = {
|
||||||
|
|
@ -477,6 +473,9 @@ module.exports = ({ services } = {}) => {
|
||||||
return ssbInstance;
|
return ssbInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const karma = require("./karma_model")({ services, ranges: { normalizeRange, widenRange, filterByRange } });
|
||||||
|
const { listAllActions, fetchUserActions, scoreFromActions, getCarbonGramsForUser, getUserEngagementScore, getLastKarmaScore, listKarmaHistory } = karma;
|
||||||
|
|
||||||
const BANKING_TYPES = ["wallet", "karmaScore", "ubiClaim", "ubiClaimResult", "ubiAllocation", "ubiRefuse", "pubAvailability", "tombstone"];
|
const BANKING_TYPES = ["wallet", "karmaScore", "ubiClaim", "ubiClaimResult", "ubiAllocation", "ubiRefuse", "pubAvailability", "tombstone"];
|
||||||
|
|
||||||
async function scanLogStream() {
|
async function scanLogStream() {
|
||||||
|
|
@ -612,316 +611,6 @@ module.exports = ({ services } = {}) => {
|
||||||
return strip(A) === strip(B);
|
return strip(A) === strip(B);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferType(c = {}) {
|
|
||||||
if (c.vote) return "vote";
|
|
||||||
if (c.votes) return "votes";
|
|
||||||
if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
|
|
||||||
if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult";
|
|
||||||
if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
|
|
||||||
if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
|
|
||||||
if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
|
|
||||||
if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
|
|
||||||
if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
|
|
||||||
if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
|
|
||||||
if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
|
|
||||||
if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
|
|
||||||
if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
|
|
||||||
if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
|
|
||||||
if (typeof c.url !== "undefined" && c.title && c.key) return "document";
|
|
||||||
if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
|
|
||||||
if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
|
|
||||||
if (typeof c.contact !== "undefined") return "contact";
|
|
||||||
if (typeof c.about !== "undefined") return "about";
|
|
||||||
if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeType(a) {
|
|
||||||
const t = a.type || a.content?.type || inferType(a.content) || "";
|
|
||||||
return String(t).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function priorityBump(p) {
|
|
||||||
const s = String(p || "").toUpperCase();
|
|
||||||
if (s === "HIGH") return 3;
|
|
||||||
if (s === "MEDIUM") return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function severityBump(s) {
|
|
||||||
const x = String(s || "").toUpperCase();
|
|
||||||
if (x === "CRITICAL") return 6;
|
|
||||||
if (x === "HIGH") return 4;
|
|
||||||
if (x === "MEDIUM") return 2;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreMarket(c) {
|
|
||||||
const st = String(c.status || "").toUpperCase();
|
|
||||||
let s = 5;
|
|
||||||
if (st === "SOLD") s += 8;
|
|
||||||
else if (st === "ACTIVE") s += 3;
|
|
||||||
const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
|
|
||||||
s += Math.min(10, bids);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreProject(c) {
|
|
||||||
const st = String(c.status || "ACTIVE").toUpperCase();
|
|
||||||
const prog = Number(c.progress || 0);
|
|
||||||
let s = 8 + Math.min(10, prog / 10);
|
|
||||||
if (st === "FUNDED") s += 10;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateOpinionScore(content) {
|
|
||||||
const cats = content?.opinions || {};
|
|
||||||
let s = 0;
|
|
||||||
for (const k in cats) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
|
|
||||||
if (k === "interesting" || k === "inspiring") s += 5;
|
|
||||||
else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3;
|
|
||||||
else s += 1;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
let actionsCache = null;
|
|
||||||
async function listAllActions() {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb) {
|
|
||||||
FEED_SRC = "none";
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const ownSeq = await ownFeedSeq(ssb);
|
|
||||||
if (actionsCache && actionsCache.ownSeq === ownSeq && Date.now() - actionsCache.ts < ACTIONS_CACHE_TTL_MS) return actionsCache.value;
|
|
||||||
const msgs = await readTyped(ssb, CONTENT_TYPES, { limit: Math.max(getLogLimit(), 20000) });
|
|
||||||
FEED_SRC = "messagesByType";
|
|
||||||
const value = msgs.map(m => {
|
|
||||||
const v = m.value || {};
|
|
||||||
const c = v.content || {};
|
|
||||||
return {
|
|
||||||
id: v.key || m.key,
|
|
||||||
author: v.author,
|
|
||||||
type: (c.type || "").toLowerCase(),
|
|
||||||
value: v,
|
|
||||||
content: c,
|
|
||||||
receivedAt: Number(m.timestamp) || 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
actionsCache = { ts: Date.now(), ownSeq, value };
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function ownFeedSeq(ssb) {
|
|
||||||
return new Promise((res) => {
|
|
||||||
try {
|
|
||||||
pull(
|
|
||||||
ssb.createUserStream({ id: ssb.id, reverse: true, limit: 1 }),
|
|
||||||
pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0))
|
|
||||||
);
|
|
||||||
} catch (_) { res(0); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function publishKarmaScore(userId, karmaScore) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb || !ssb.publish) return false;
|
|
||||||
if (!(await ownFeedSeq(ssb))) return false;
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const content = { type: "karmaScore", karmaScore, userId, timestamp };
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUserActions(userId) {
|
|
||||||
const me = resolveUserId(userId);
|
|
||||||
const actions = await listAllActions();
|
|
||||||
const authored = actions.filter(a =>
|
|
||||||
(a.author && a.author === me) || (a.value?.author && a.value.author === me)
|
|
||||||
);
|
|
||||||
if (authored.length) return authored;
|
|
||||||
return actions.filter(a => {
|
|
||||||
const c = a.content || {};
|
|
||||||
const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
|
|
||||||
return fields.some(f => f && f === me);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function basePointsFor(action) {
|
|
||||||
const t = normalizeType(action);
|
|
||||||
const c = action.content || {};
|
|
||||||
const rawType = String(c.type || "").toLowerCase();
|
|
||||||
if (t === "post") return 10;
|
|
||||||
else if (t === "comment") return 5;
|
|
||||||
else if (t === "like") return 2;
|
|
||||||
else if (t === "image") return 8;
|
|
||||||
else if (t === "video") return 12;
|
|
||||||
else if (t === "audio") return 8;
|
|
||||||
else if (t === "document") return 6;
|
|
||||||
else if (t === "bookmark") return 2;
|
|
||||||
else if (t === "feed") return 6;
|
|
||||||
else if (t === "forum") return (c.root ? 5 : 10);
|
|
||||||
else if (t === "vote") return (3 + calculateOpinionScore(c));
|
|
||||||
else if (t === "votes") return Math.min(10, Number(c.totalVotes || 0));
|
|
||||||
else if (t === "market") return scoreMarket(c);
|
|
||||||
else if (t === "project") return scoreProject(c);
|
|
||||||
else if (t === "tribe") return (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0));
|
|
||||||
else if (t === "event") return (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0));
|
|
||||||
else if (t === "task") return (3 + priorityBump(c.priority));
|
|
||||||
else if (t === "report") return (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity));
|
|
||||||
else if (t === "curriculum") return 5;
|
|
||||||
else if (t === "wikipage") return (c.replaces ? 2 : 6);
|
|
||||||
else if (t === "emergency") return (c.replaces ? 1 : 5);
|
|
||||||
else if (t === "emergencyconfirm") return 1;
|
|
||||||
else if (t === "emergencyupdate") return 2;
|
|
||||||
else if (t === "mailinglist") return (c.replaces ? 1 : 3);
|
|
||||||
else if (t === "logisticsroute") return (c.replaces ? 1 : 4);
|
|
||||||
else if (t === "logisticsrating") return 1;
|
|
||||||
else if (t === "podcast") return (c.replaces ? 1 : 5);
|
|
||||||
else if (t === "podcastepisode") return (c.replaces ? 1 : 4);
|
|
||||||
else if (t === "podcastopinion" || t === "podcastplay" || t === "campaignopinion" || t === "logisticsopinion") return 1;
|
|
||||||
else if (t === "campaign") return (c.replaces ? 1 : 5);
|
|
||||||
else if (t === "campaignsignature") return 1;
|
|
||||||
else if (t === "campaignupdate") return 2;
|
|
||||||
else if (t === "aiexchange") return (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0);
|
|
||||||
else if (t === "job") return (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0));
|
|
||||||
else if (t === "transfer") return 1;
|
|
||||||
else if (t === "about") return (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0));
|
|
||||||
else if (t === "contact") return 1;
|
|
||||||
else if (t === "pub") return 1;
|
|
||||||
else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") return 12;
|
|
||||||
else if (t === "parliamentterm" || rawType === "parliamentterm") return 25;
|
|
||||||
else if (t === "parliamentproposal" || rawType === "parliamentproposal") return 8;
|
|
||||||
else if (t === "parliamentlaw" || rawType === "parliamentlaw") return 16;
|
|
||||||
else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") return 10;
|
|
||||||
else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") return 4;
|
|
||||||
else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") return 3;
|
|
||||||
else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") return 4;
|
|
||||||
else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") return 10;
|
|
||||||
else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") return 8;
|
|
||||||
else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") return 6;
|
|
||||||
else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") return 3;
|
|
||||||
else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") return 1;
|
|
||||||
else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") return 6;
|
|
||||||
else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") return 2;
|
|
||||||
else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") return 3;
|
|
||||||
else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") return 5;
|
|
||||||
else if (t === "larpjoinhouse") return 15;
|
|
||||||
else if (t === "larphousepost") return 6;
|
|
||||||
else if (t === "larptestattempt") return 3;
|
|
||||||
else if (t === "torrent") return 6;
|
|
||||||
else if (t === "file") return 6;
|
|
||||||
else if (t === "shop" || t === "shopproduct") return 6;
|
|
||||||
else if (t === "shop-purchase") return 2;
|
|
||||||
else if (t === "pad" || t === "padentry") return 3;
|
|
||||||
else if (t === "room") return 3;
|
|
||||||
else if (t === "roommember") return 1;
|
|
||||||
else if (t === "calendar" || t === "calendarnote" || t === "calendardate") return 3;
|
|
||||||
else if (t === "chat") return 1;
|
|
||||||
else if (t === "gamescore") return 2;
|
|
||||||
else if (t === "pixelia") return 2;
|
|
||||||
else if (rawType === "industry") return 8;
|
|
||||||
else if (rawType === "industryblueprint") return 6;
|
|
||||||
else if (rawType === "industrybuild") return 8;
|
|
||||||
else if (rawType === "industrycontribution") return 4;
|
|
||||||
else if (rawType === "industryallocation") return 10;
|
|
||||||
else if (rawType === "industryvote") return 2;
|
|
||||||
else if (rawType === "industrymember") return 2;
|
|
||||||
else if (rawType === "industryopinion") return 1;
|
|
||||||
else if (rawType === "schoolcourse") return 10;
|
|
||||||
else if (rawType === "schoollesson") return 6;
|
|
||||||
else if (rawType === "schoolenroll") return 3;
|
|
||||||
else if (rawType === "schoolcertificate") return 12;
|
|
||||||
else if (rawType === "schoolexam") return 6;
|
|
||||||
else if (rawType === "schoolprogress") return 1;
|
|
||||||
else if (rawType === "schoolopinion") return 2;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decayFor(ts, nowMs) {
|
|
||||||
const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity;
|
|
||||||
return ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreFromActions(actions) {
|
|
||||||
let score = 0;
|
|
||||||
const nowMs = Date.now();
|
|
||||||
const perTypeDay = new Map();
|
|
||||||
for (const action of actions) {
|
|
||||||
const pts = basePointsFor(action);
|
|
||||||
if (!pts) continue;
|
|
||||||
const ts = action.value?.timestamp;
|
|
||||||
const dayKey = `${normalizeType(action)}:${ts ? new Date(ts).toISOString().slice(0, 10) : "?"}`;
|
|
||||||
const n = (perTypeDay.get(dayKey) || 0) + 1;
|
|
||||||
perTypeDay.set(dayKey, n);
|
|
||||||
score += pts * decayFor(ts, nowMs) / n;
|
|
||||||
}
|
|
||||||
return Math.max(0, Math.round(Math.min(OWN_SCORE_CAP, score)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function receivedScoreFor(userId, actions) {
|
|
||||||
const nowMs = Date.now();
|
|
||||||
const mine = new Set();
|
|
||||||
const firstTs = new Map();
|
|
||||||
for (const a of actions) {
|
|
||||||
if (!a.author) continue;
|
|
||||||
const ts = Number(a.receivedAt) || 0;
|
|
||||||
if (ts && (!firstTs.has(a.author) || ts < firstTs.get(a.author))) firstTs.set(a.author, ts);
|
|
||||||
if (a.author === userId && a.id) mine.add(a.id);
|
|
||||||
}
|
|
||||||
const seen = new Set();
|
|
||||||
const perTarget = new Map();
|
|
||||||
let score = 0;
|
|
||||||
const add = (author, kind, target, pts, ts, capPerTarget = Infinity) => {
|
|
||||||
const k = `${kind}:${author}:${target}`;
|
|
||||||
if (seen.has(k)) return;
|
|
||||||
const n = perTarget.get(`${kind}:${target}`) || 0;
|
|
||||||
if (n >= capPerTarget) return;
|
|
||||||
seen.add(k);
|
|
||||||
perTarget.set(`${kind}:${target}`, n + 1);
|
|
||||||
score += pts * decayFor(ts, nowMs);
|
|
||||||
};
|
|
||||||
for (const a of actions) {
|
|
||||||
const b = a.author;
|
|
||||||
if (!b || b === userId) continue;
|
|
||||||
if (!firstTs.has(b) || nowMs - firstTs.get(b) < RECEIVED_AUTHOR_MIN_AGE_MS) continue;
|
|
||||||
const c = a.content || {};
|
|
||||||
const type = String(c.type || "");
|
|
||||||
const ts = a.value?.timestamp;
|
|
||||||
if (/Opinion$/.test(type) && type !== "pollOpinion" && mine.has(c.target)) add(b, "opinion", c.target, NEGATIVE_OPINIONS.has(String(c.category || "").toLowerCase()) ? -3 : 3, ts);
|
|
||||||
else if (type === "vote" && c.vote && mine.has(c.vote.link)) add(b, "opinion", c.vote.link, Number(c.vote.value) > 0 ? 3 : -3, ts);
|
|
||||||
else if (type === "feed-action" && c.action === "vote" && mine.has(c.root || c.target)) add(b, "opinion", c.root || c.target, 3, ts);
|
|
||||||
else if (type === "feed-action" && c.action === "comment" && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10);
|
|
||||||
else if (type === "feed-action" && c.action === "refeed" && mine.has(c.root)) add(b, "refeed", c.root, 2, ts, 10);
|
|
||||||
else if (type === "post" && c.root && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10);
|
|
||||||
else if (type === "contact" && c.following === true && c.contact === userId) add(b, "follow", userId, 2, ts);
|
|
||||||
else if (type === "marketPurchase" && mine.has(c.target)) add(b, "purchase", c.target, 5, ts);
|
|
||||||
else if (type === "shop-purchase" && c.seller === userId) add(b, "purchase", c.productId || a.id, 5, ts);
|
|
||||||
else if (type === "schoolEnroll" && c.value === true && mine.has(c.courseId)) add(b, "enroll", c.courseId, 5, ts);
|
|
||||||
else if (type === "transferConfirm" && mine.has(c.target)) add(b, "confirm", c.target, 3, ts);
|
|
||||||
}
|
|
||||||
return Math.max(0, Math.round(score));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCarbonGramsForUser(userId) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb || !userId) return 0;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let bytes = 0;
|
|
||||||
pull(
|
|
||||||
ssb.createUserStream({ id: userId }),
|
|
||||||
pull.drain(
|
|
||||||
(m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} },
|
|
||||||
() => resolve((bytes / (1024 * 1024)) * 0.095)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBytesForUser(userId) {
|
async function getBytesForUser(userId) {
|
||||||
const ssb = await openSsb();
|
const ssb = await openSsb();
|
||||||
if (!ssb || !userId) return 0;
|
if (!ssb || !userId) return 0;
|
||||||
|
|
@ -1134,85 +823,6 @@ async function getSchoolBalance(userId) {
|
||||||
return { received: Number(received.toFixed(6)), lifetime: Number(lifetime.toFixed(6)), net: Number(received.toFixed(6)) };
|
return { received: Number(received.toFixed(6)), lifetime: Number(lifetime.toFixed(6)), net: Number(received.toFixed(6)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserEngagementScore(userId) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
const uid = resolveUserId(userId);
|
|
||||||
const actions = await fetchUserActions(uid);
|
|
||||||
const all = await listAllActions();
|
|
||||||
const rawKarma = scoreFromActions(actions) + receivedScoreFor(uid, all);
|
|
||||||
const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0);
|
|
||||||
const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams));
|
|
||||||
|
|
||||||
const prev = await getLastKarmaScore(uid);
|
|
||||||
const lastPublishedTimestamp = await getLastPublishedTimestamp(uid);
|
|
||||||
|
|
||||||
const isSelf = idsEqual(uid, ssb.id);
|
|
||||||
const hasSSB = !!(ssb && ssb.publish);
|
|
||||||
|
|
||||||
const changed = (prev === null) || (karmaScore !== prev);
|
|
||||||
const nowMs = Date.now();
|
|
||||||
const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0;
|
|
||||||
const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (isSelf && hasSSB && changed && cooldownOk) {
|
|
||||||
await publishKarmaScore(uid, karmaScore);
|
|
||||||
}
|
|
||||||
return karmaScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLastKarmaScore(userId) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb) return null;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const source = ssb.messagesByType
|
|
||||||
? ssb.messagesByType({ type: "karmaScore", reverse: true })
|
|
||||||
: ssb.createLogStream && ssb.createLogStream({ reverse: true });
|
|
||||||
if (!source) return resolve(null);
|
|
||||||
pull(
|
|
||||||
source,
|
|
||||||
pull.filter(msg => {
|
|
||||||
const v = msg.value || msg;
|
|
||||||
const c = v.content || {};
|
|
||||||
return c && c.type === "karmaScore" && c.userId === userId;
|
|
||||||
}),
|
|
||||||
pull.take(1),
|
|
||||||
pull.collect((err, arr) => {
|
|
||||||
if (err || !arr || !arr.length) return resolve(null);
|
|
||||||
const v = arr[0].value || arr[0];
|
|
||||||
const c = v.content || {};
|
|
||||||
resolve(Number(c.karmaScore) || 0);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLastPublishedTimestamp(userId) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb) return new Date(0).toISOString();
|
|
||||||
const fallback = new Date(0).toISOString();
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const source = ssb.messagesByType
|
|
||||||
? ssb.messagesByType({ type: "karmaScore", reverse: true })
|
|
||||||
: ssb.createLogStream && ssb.createLogStream({ reverse: true });
|
|
||||||
if (!source) return resolve(fallback);
|
|
||||||
pull(
|
|
||||||
source,
|
|
||||||
pull.filter(msg => {
|
|
||||||
const v = msg.value || msg;
|
|
||||||
const c = v.content || {};
|
|
||||||
return c && c.type === "karmaScore" && c.userId === userId;
|
|
||||||
}),
|
|
||||||
pull.take(1),
|
|
||||||
pull.collect((err, arr) => {
|
|
||||||
if (err || !arr || !arr.length) return resolve(fallback);
|
|
||||||
const v = arr[0].value || arr[0];
|
|
||||||
const c = v.content || {};
|
|
||||||
resolve(c.timestamp || fallback);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function computePoolVars(pubBal, rules) {
|
function computePoolVars(pubBal, rules) {
|
||||||
const alphaCap = (rules.alpha ?? DEFAULT_RULES.alpha) * pubBal;
|
const alphaCap = (rules.alpha ?? DEFAULT_RULES.alpha) * pubBal;
|
||||||
const available = Math.max(0, pubBal - (rules.reserveMin ?? DEFAULT_RULES.reserveMin));
|
const available = Math.max(0, pubBal - (rules.reserveMin ?? DEFAULT_RULES.reserveMin));
|
||||||
|
|
@ -1820,20 +1430,6 @@ async function getLastPublishedTimestamp(userId) {
|
||||||
return { points: filterByRange(all, effective).slice(-240), hasAny: all.length >= 2, range: effective };
|
return { points: filterByRange(all, effective).slice(-240), hasAny: all.length >= 2, range: effective };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listKarmaHistory(userId, range = "all", explicit = true) {
|
|
||||||
const ssb = await openSsb();
|
|
||||||
if (!ssb) return { points: [], hasAny: false, range: normalizeRange(range) };
|
|
||||||
const uid = resolveUserId(userId);
|
|
||||||
const msgs = await readTyped(ssb, ["karmaScore"], { limit: Math.max(getLogLimit(), 5000) });
|
|
||||||
const all = msgs
|
|
||||||
.filter(m => m.value && m.value.author === uid && m.value.content && m.value.content.type === "karmaScore")
|
|
||||||
.map(m => ({ ts: Date.parse(m.value.content.timestamp || "") || Number(m.value.timestamp) || 0, score: Number(m.value.content.karmaScore) || 0 }))
|
|
||||||
.filter(x => x.ts > 0)
|
|
||||||
.sort((a, b) => a.ts - b.ts);
|
|
||||||
const effective = explicit ? normalizeRange(range) : widenRange(all, range);
|
|
||||||
return { points: filterByRange(all, effective).slice(-120), hasAny: all.length >= 2, range: effective };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listUbiPubs() {
|
async function listUbiPubs() {
|
||||||
const latestByPub = await scanUbiAnnouncements();
|
const latestByPub = await scanUbiAnnouncements();
|
||||||
const trusted = await trustedPubIds();
|
const trusted = await trustedPubIds();
|
||||||
|
|
@ -2197,6 +1793,8 @@ async function getLastPublishedTimestamp(userId) {
|
||||||
async function getBankingData(userId) {
|
async function getBankingData(userId) {
|
||||||
const ecoValue = await perRequest("ecoinValue", () => calculateEcoinValue());
|
const ecoValue = await perRequest("ecoinValue", () => calculateEcoinValue());
|
||||||
const karmaScore = await getUserEngagementScore(userId);
|
const karmaScore = await getUserEngagementScore(userId);
|
||||||
|
const received = await karma.getKarmaBreakdown(userId);
|
||||||
|
const karmaReceived = { positive: received.positive, negative: received.negative };
|
||||||
let estimatedUBI = 0;
|
let estimatedUBI = 0;
|
||||||
try {
|
try {
|
||||||
const pubBal = isPubNode() ? await perRequest("pubBalance", () => safeGetBalance("pub")) : 0;
|
const pubBal = isPubNode() ? await perRequest("pubBalance", () => safeGetBalance("pub")) : 0;
|
||||||
|
|
@ -2227,6 +1825,7 @@ async function getLastPublishedTimestamp(userId) {
|
||||||
return {
|
return {
|
||||||
ecoValue,
|
ecoValue,
|
||||||
karmaScore,
|
karmaScore,
|
||||||
|
karmaReceived,
|
||||||
estimatedUBI,
|
estimatedUBI,
|
||||||
estimatedUBIBeforeTax: Number(estimatedUBIBeforeTax.toFixed(6)),
|
estimatedUBIBeforeTax: Number(estimatedUBIBeforeTax.toFixed(6)),
|
||||||
userEcoinTax: Number(userEcoinTax.toFixed(6)),
|
userEcoinTax: Number(userEcoinTax.toFixed(6)),
|
||||||
|
|
@ -2636,6 +2235,7 @@ async function getLastPublishedTimestamp(userId) {
|
||||||
computeEpoch,
|
computeEpoch,
|
||||||
executeEpoch,
|
executeEpoch,
|
||||||
getUserEngagementScore,
|
getUserEngagementScore,
|
||||||
|
getKarmaReceived: async (userId) => { const r = karma.receivedBreakdownFor(String(userId || config.keys.id), await listAllActions()); return { positive: r.positive, negative: r.negative }; },
|
||||||
publishBankClaim,
|
publishBankClaim,
|
||||||
publishUbiAllocation,
|
publishUbiAllocation,
|
||||||
publishUbiClaim,
|
publishUbiClaim,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
text: longText.joinText(c.text || '', c.chunks, longText.lookupIn(idx.chunks), node.author),
|
text: longText.joinText(c.text || '', c.chunks, longText.lookupIn(idx.chunks), node.author),
|
||||||
mentions: Array.isArray(c.mentions) ? c.mentions : [],
|
mentions: Array.isArray(c.mentions) ? c.mentions : [],
|
||||||
allowComments: c.allowComments !== false,
|
allowComments: c.allowComments !== false,
|
||||||
|
tags: Array.isArray(c.tags) ? c.tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean) : [],
|
||||||
createdAt: new Date(node.ts).toISOString(),
|
createdAt: new Date(node.ts).toISOString(),
|
||||||
commentCount: idx.commentsByRoot.get(node.key) || 0,
|
commentCount: idx.commentsByRoot.get(node.key) || 0,
|
||||||
opinions: op.counts,
|
opinions: op.counts,
|
||||||
|
|
@ -99,7 +100,8 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
|
|
||||||
const q = String(opts.q || '').trim().toLowerCase();
|
const q = String(opts.q || '').trim().toLowerCase();
|
||||||
if (q) out = out.filter(b =>
|
if (q) out = out.filter(b =>
|
||||||
b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q));
|
b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q) ||
|
||||||
|
b.tags.some(t => t.toLowerCase().includes(q.replace(/^#/, ''))));
|
||||||
|
|
||||||
if (f === 'TOP') {
|
if (f === 'TOP') {
|
||||||
out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) ||
|
out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) ||
|
||||||
|
|
@ -118,7 +120,7 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
return blog;
|
return blog;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createBlog({ text, subject = '', mentions = [], allowComments = true } = {}) {
|
async createBlog({ text, subject = '', mentions = [], allowComments = true, tags = [] } = {}) {
|
||||||
if (isPublic) throw new Error('Not available in public mode');
|
if (isPublic) throw new Error('Not available in public mode');
|
||||||
const body = String(text || '').trim();
|
const body = String(text || '').trim();
|
||||||
if (!body) throw new Error('Blog text is required');
|
if (!body) throw new Error('Blog text is required');
|
||||||
|
|
@ -128,6 +130,7 @@ module.exports = ({ cooler, isPublic = false }) => {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: body,
|
text: body,
|
||||||
allowComments: allowComments !== false,
|
allowComments: allowComments !== false,
|
||||||
|
...(Array.isArray(tags) && tags.length ? { tags: Array.from(new Set(tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean))) } : {}),
|
||||||
...(Array.isArray(mentions) && mentions.length ? { mentions } : {}),
|
...(Array.isArray(mentions) && mentions.length ? { mentions } : {}),
|
||||||
...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {})
|
...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {})
|
||||||
}, 'text', { publish });
|
}, 'text', { publish });
|
||||||
|
|
|
||||||
|
|
@ -367,11 +367,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
if (c.type !== "chatMessage") return null
|
if (c.type !== "chatMessage") return null
|
||||||
|
|
||||||
let text = c.text || ""
|
let text = c.text || ""
|
||||||
|
let image = c.image || null
|
||||||
|
let mimeType = typeof c.mimeType === "string" ? c.mimeType : null
|
||||||
if (tribeCrypto && c.encryptedText) {
|
if (tribeCrypto && 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 = tribeCrypto.decryptWithKey(c.encryptedText, keyHex)
|
||||||
|
if (c.encryptedImage) image = tribeCrypto.decryptWithKey(c.encryptedImage, keyHex)
|
||||||
|
if (c.encryptedMime) mimeType = tribeCrypto.decryptWithKey(c.encryptedMime, keyHex)
|
||||||
break
|
break
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -381,8 +385,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
key: node.key,
|
key: node.key,
|
||||||
chatId: c.chatId || "",
|
chatId: c.chatId || "",
|
||||||
text,
|
text,
|
||||||
image: c.image || null,
|
image,
|
||||||
mimeType: typeof c.mimeType === "string" ? c.mimeType : null,
|
mimeType,
|
||||||
replyTo: typeof c.replyTo === "string" ? c.replyTo : null,
|
replyTo: typeof c.replyTo === "string" ? c.replyTo : null,
|
||||||
author: node.author,
|
author: node.author,
|
||||||
createdAt: c.createdAt || new Date(node.ts).toISOString()
|
createdAt: c.createdAt || new Date(node.ts).toISOString()
|
||||||
|
|
@ -962,6 +966,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
||||||
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 = tribeCrypto.encryptWithKey(safeText(text), encKey)
|
||||||
|
if (content.image) {
|
||||||
|
content.encryptedImage = tribeCrypto.encryptWithKey(String(content.image), encKey)
|
||||||
|
delete content.image
|
||||||
|
if (content.mimeType) {
|
||||||
|
content.encryptedMime = tribeCrypto.encryptWithKey(String(content.mimeType), encKey)
|
||||||
|
delete content.mimeType
|
||||||
|
}
|
||||||
|
}
|
||||||
if (chat.tribeId) content.tribeId = chat.tribeId
|
if (chat.tribeId) content.tribeId = chat.tribeId
|
||||||
} else {
|
} else {
|
||||||
content.text = safeText(text)
|
content.text = safeText(text)
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,15 @@ const KINDS = {
|
||||||
mailing: { type: 'mailingList', href: (id) => `/mailing/${encodeURIComponent(id)}` },
|
mailing: { type: 'mailingList', href: (id) => `/mailing/${encodeURIComponent(id)}` },
|
||||||
logistics: { type: 'logisticsRoute', href: (id) => `/logistics/${encodeURIComponent(id)}` },
|
logistics: { type: 'logisticsRoute', href: (id) => `/logistics/${encodeURIComponent(id)}` },
|
||||||
podcasts: { type: 'podcast', href: (id) => `/podcasts/${encodeURIComponent(id)}` },
|
podcasts: { type: 'podcast', href: (id) => `/podcasts/${encodeURIComponent(id)}` },
|
||||||
campaigns: { type: 'campaign', href: (id) => `/campaigns/${encodeURIComponent(id)}` }
|
campaigns: { type: 'campaign', href: (id) => `/campaigns/${encodeURIComponent(id)}` },
|
||||||
|
blogs: { type: 'post', href: (id) => `/blogs/${encodeURIComponent(id)}` }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRootBlogPost = (c) =>
|
||||||
|
!!c && c.type === 'post' && typeof c.text === 'string' &&
|
||||||
|
!c.root && !c.fork && !c.branch && !c.about && !c.target &&
|
||||||
|
c.private !== true && !Array.isArray(c.recps);
|
||||||
|
|
||||||
const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k]));
|
const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k]));
|
||||||
|
|
||||||
const cvSkills = (c) => [
|
const cvSkills = (c) => [
|
||||||
|
|
@ -103,7 +109,7 @@ const opinionsTotalOf = (c) => {
|
||||||
const excerpt = (s) => String(s || '').replace(/!?\[[^\]]*\]\([^)]*\)/g, ' ').replace(/[#*_>`~|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 90);
|
const excerpt = (s) => String(s || '').replace(/!?\[[^\]]*\]\([^)]*\)/g, ' ').replace(/[#*_>`~|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 90);
|
||||||
const titleOf = (kind, c, author) => {
|
const titleOf = (kind, c, author) => {
|
||||||
if (kind === 'inhabitants') return c.name || '';
|
if (kind === 'inhabitants') return c.name || '';
|
||||||
return c.title || c.name || c.question || c.concept || c.subject || excerpt(typeof c.text === 'string' ? c.text : '') || excerpt(typeof c.description === 'string' ? c.description : '') || '';
|
return c.title || c.name || c.question || c.concept || c.subject || c.contentWarning || excerpt(typeof c.text === 'string' ? c.text : '') || excerpt(typeof c.description === 'string' ? c.description : '') || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const jaccard = (a, b) => {
|
const jaccard = (a, b) => {
|
||||||
|
|
@ -147,6 +153,7 @@ module.exports = ({ cooler, favoriteIdsFor = null }) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (c.tribeId && c.type !== 'tribe') continue;
|
if (c.tribeId && c.type !== 'tribe') continue;
|
||||||
|
if (c.type === 'post' && !isRootBlogPost(c)) continue;
|
||||||
const kind = KIND_BY_TYPE[c.type];
|
const kind = KIND_BY_TYPE[c.type];
|
||||||
if (!kind) continue;
|
if (!kind) continue;
|
||||||
versions.push(versionEntry(m));
|
versions.push(versionEntry(m));
|
||||||
|
|
@ -360,12 +367,20 @@ module.exports = ({ cooler, favoriteIdsFor = null }) => {
|
||||||
out.push({ ...strip(n), score, common, connections: common.length, reasons });
|
out.push({ ...strip(n), score, common, connections: common.length, reasons });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.hidden && (opts.hidden.authors || opts.hidden.ids)) {
|
||||||
|
const ha = opts.hidden.authors instanceof Set ? opts.hidden.authors : new Set();
|
||||||
|
const hi = opts.hidden.ids instanceof Set ? opts.hidden.ids : new Set();
|
||||||
|
out = out.filter(s => !ha.has(String(s.author)) && !hi.has(String(s.id)));
|
||||||
|
}
|
||||||
const anyMatches = out.length > 0;
|
const anyMatches = out.length > 0;
|
||||||
const kindsAvail = {};
|
const kindsAvail = {};
|
||||||
for (const n of use) {
|
for (const n of use) {
|
||||||
if (String(n.author) !== String(viewerId)) kindsAvail[n.kind] = true;
|
if (String(n.author) !== String(viewerId)) kindsAvail[n.kind] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kindsMatched = {};
|
||||||
|
for (const s of out) kindsMatched[s.kind] = true;
|
||||||
|
|
||||||
if (f === 'RECENT') {
|
if (f === 'RECENT') {
|
||||||
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
||||||
out = out.filter(s => s.ts >= cutoff);
|
out = out.filter(s => s.ts >= cutoff);
|
||||||
|
|
@ -393,6 +408,7 @@ module.exports = ({ cooler, favoriteIdsFor = null }) => {
|
||||||
hasProfile: myTermSet.size > 0,
|
hasProfile: myTermSet.size > 0,
|
||||||
myTerms: [...myTermSet],
|
myTerms: [...myTermSet],
|
||||||
kindsAvail,
|
kindsAvail,
|
||||||
|
kindsMatched,
|
||||||
anyMatches,
|
anyMatches,
|
||||||
reasonsAvail,
|
reasonsAvail,
|
||||||
reason: reasonsAvail.includes(reason) ? reason : ''
|
reason: reasonsAvail.includes(reason) ? reason : ''
|
||||||
|
|
|
||||||
591
src/models/karma_model.js
Normal file
591
src/models/karma_model.js
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
const pull = require("../server/node_modules/pull-stream");
|
||||||
|
const { readTyped, CONTENT_TYPES } = require("./typed_log");
|
||||||
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
|
const { config } = require("../server/SSB_server.js");
|
||||||
|
|
||||||
|
function getLogLimit() {
|
||||||
|
return getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OWN_SCORE_CAP = 200;
|
||||||
|
const RECEIVED_AUTHOR_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
const opinionCategories = require("../backend/opinion_categories");
|
||||||
|
const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive);
|
||||||
|
const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation);
|
||||||
|
const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral);
|
||||||
|
const NEGATIVE_MIN_VOTERS = 3;
|
||||||
|
const KARMA_PERIOD_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
const NEGATIVE_PATTERN_MIN_VOTERS = 5;
|
||||||
|
const NEGATIVE_PATTERN_POINTS = 2;
|
||||||
|
const POSITIVE_PER_VOTER_CAP = 10;
|
||||||
|
const TRUST_MIN_FOLLOWERS = 2;
|
||||||
|
const REACTIONS_PER_MESSAGE_CAP = 10;
|
||||||
|
const PLAYS_PER_EPISODE_CAP = 20;
|
||||||
|
const ACTIONS_CACHE_TTL_MS = 60 * 1000;
|
||||||
|
|
||||||
|
function resolveUserId(maybeId) {
|
||||||
|
const s = String(maybeId || "").trim();
|
||||||
|
if (s) return s;
|
||||||
|
return config?.keys?.id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function idsEqual(a, b) {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
const A = String(a).trim();
|
||||||
|
const B = String(b).trim();
|
||||||
|
if (A === B) return true;
|
||||||
|
const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, "");
|
||||||
|
return strip(A) === strip(B);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferType(c = {}) {
|
||||||
|
if (c.vote) return "vote";
|
||||||
|
if (c.votes) return "votes";
|
||||||
|
if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
|
||||||
|
if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult";
|
||||||
|
if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
|
||||||
|
if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
|
||||||
|
if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
|
||||||
|
if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
|
||||||
|
if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
|
||||||
|
if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
|
||||||
|
if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
|
||||||
|
if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
|
||||||
|
if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
|
||||||
|
if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
|
||||||
|
if (typeof c.url !== "undefined" && c.title && c.key) return "document";
|
||||||
|
if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
|
||||||
|
if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
|
||||||
|
if (typeof c.contact !== "undefined") return "contact";
|
||||||
|
if (typeof c.about !== "undefined") return "about";
|
||||||
|
if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeType(a) {
|
||||||
|
const t = a.type || a.content?.type || inferType(a.content) || "";
|
||||||
|
return String(t).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function priorityBump(p) {
|
||||||
|
const s = String(p || "").toUpperCase();
|
||||||
|
if (s === "HIGH") return 3;
|
||||||
|
if (s === "MEDIUM") return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function severityBump(s) {
|
||||||
|
const x = String(s || "").toUpperCase();
|
||||||
|
if (x === "CRITICAL") return 6;
|
||||||
|
if (x === "HIGH") return 4;
|
||||||
|
if (x === "MEDIUM") return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreMarket(c) {
|
||||||
|
const st = String(c.status || "").toUpperCase();
|
||||||
|
let s = 5;
|
||||||
|
if (st === "SOLD") s += 8;
|
||||||
|
else if (st === "ACTIVE") s += 3;
|
||||||
|
const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
|
||||||
|
s += Math.min(10, bids);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreProject(c) {
|
||||||
|
const st = String(c.status || "ACTIVE").toUpperCase();
|
||||||
|
const prog = Number(c.progress || 0);
|
||||||
|
let s = 8 + Math.min(10, prog / 10);
|
||||||
|
if (st === "FUNDED") s += 10;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOpinionScore(content) {
|
||||||
|
const cats = content?.opinions || {};
|
||||||
|
let s = 0;
|
||||||
|
for (const k in cats) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
|
||||||
|
if (NEUTRAL_OPINIONS.has(k)) continue;
|
||||||
|
if (k === "interesting" || k === "inspiring") s += 5;
|
||||||
|
else if (MODERATION_OPINIONS.has(k)) s -= 5;
|
||||||
|
else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3;
|
||||||
|
else s += 1;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function basePointsFor(action) {
|
||||||
|
const t = normalizeType(action);
|
||||||
|
const c = action.content || {};
|
||||||
|
const rawType = String(c.type || "").toLowerCase();
|
||||||
|
if (t === "post") return 10;
|
||||||
|
else if (t === "comment") return 5;
|
||||||
|
else if (t === "like") return 2;
|
||||||
|
else if (t === "image") return 8;
|
||||||
|
else if (t === "video") return 12;
|
||||||
|
else if (t === "audio") return 8;
|
||||||
|
else if (t === "document") return 6;
|
||||||
|
else if (t === "bookmark") return 2;
|
||||||
|
else if (t === "feed") return 6;
|
||||||
|
else if (t === "forum") return (c.root ? 5 : 10);
|
||||||
|
else if (t === "vote") return (3 + calculateOpinionScore(c));
|
||||||
|
else if (t === "votes") return Math.min(10, Number(c.totalVotes || 0));
|
||||||
|
else if (t === "market") return scoreMarket(c);
|
||||||
|
else if (t === "project") return scoreProject(c);
|
||||||
|
else if (t === "tribe") return (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0));
|
||||||
|
else if (t === "event") return (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0));
|
||||||
|
else if (t === "task") return (3 + priorityBump(c.priority));
|
||||||
|
else if (t === "report") return (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity));
|
||||||
|
else if (t === "curriculum") return 5;
|
||||||
|
else if (t === "wikipage") return (c.replaces ? 2 : 6);
|
||||||
|
else if (t === "emergency") return (c.replaces ? 1 : 5);
|
||||||
|
else if (t === "emergencyconfirm") return 1;
|
||||||
|
else if (t === "emergencyupdate") return 2;
|
||||||
|
else if (t === "mailinglist") return (c.replaces ? 1 : 3);
|
||||||
|
else if (t === "logisticsroute") return (c.replaces ? 1 : 4);
|
||||||
|
else if (t === "logisticsrating") return 1;
|
||||||
|
else if (t === "podcast") return (c.replaces ? 1 : 5);
|
||||||
|
else if (t === "podcastepisode") return (c.replaces ? 1 : 4);
|
||||||
|
else if (t === "podcastopinion" || t === "podcastplay" || t === "campaignopinion" || t === "logisticsopinion") return 1;
|
||||||
|
else if (t === "campaign") return (c.replaces ? 1 : 5);
|
||||||
|
else if (t === "campaignsignature") return 1;
|
||||||
|
else if (t === "campaignupdate") return 2;
|
||||||
|
else if (t === "aiexchange") return (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0);
|
||||||
|
else if (t === "job") return (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0));
|
||||||
|
else if (t === "transfer") return 1;
|
||||||
|
else if (t === "about") return (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0));
|
||||||
|
else if (t === "contact") return 1;
|
||||||
|
else if (t === "pub") return 1;
|
||||||
|
else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") return 12;
|
||||||
|
else if (t === "parliamentterm" || rawType === "parliamentterm") return 25;
|
||||||
|
else if (t === "parliamentproposal" || rawType === "parliamentproposal") return 8;
|
||||||
|
else if (t === "parliamentlaw" || rawType === "parliamentlaw") return 16;
|
||||||
|
else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") return 10;
|
||||||
|
else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") return 4;
|
||||||
|
else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") return 3;
|
||||||
|
else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") return 4;
|
||||||
|
else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") return 10;
|
||||||
|
else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") return 8;
|
||||||
|
else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") return 6;
|
||||||
|
else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") return 3;
|
||||||
|
else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") return 1;
|
||||||
|
else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") return 6;
|
||||||
|
else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") return 2;
|
||||||
|
else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") return 3;
|
||||||
|
else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") return 5;
|
||||||
|
else if (t === "larpjoinhouse") return 15;
|
||||||
|
else if (t === "larphousepost") return 6;
|
||||||
|
else if (t === "larptestattempt") return 3;
|
||||||
|
else if (t === "torrent") return 6;
|
||||||
|
else if (t === "file") return 6;
|
||||||
|
else if (t === "shop" || t === "shopproduct") return 6;
|
||||||
|
else if (t === "shop-purchase") return 2;
|
||||||
|
else if (t === "pad" || t === "padentry") return 3;
|
||||||
|
else if (t === "room") return 3;
|
||||||
|
else if (t === "roommember") return 1;
|
||||||
|
else if (t === "calendar" || t === "calendarnote" || t === "calendardate") return 3;
|
||||||
|
else if (t === "chat") return 1;
|
||||||
|
else if (t === "gamescore") return 2;
|
||||||
|
else if (t === "pixelia") return 2;
|
||||||
|
else if (rawType === "industry") return 8;
|
||||||
|
else if (rawType === "industryblueprint") return 6;
|
||||||
|
else if (rawType === "industrybuild") return 8;
|
||||||
|
else if (rawType === "industrycontribution") return 4;
|
||||||
|
else if (rawType === "industryallocation") return 10;
|
||||||
|
else if (rawType === "industryvote") return 2;
|
||||||
|
else if (rawType === "industrymember") return 2;
|
||||||
|
else if (rawType === "industryopinion") return 1;
|
||||||
|
else if (rawType === "schoolcourse") return 10;
|
||||||
|
else if (rawType === "schoollesson") return 6;
|
||||||
|
else if (rawType === "schoolenroll") return 3;
|
||||||
|
else if (rawType === "schoolcertificate") return 12;
|
||||||
|
else if (rawType === "schoolexam") return 6;
|
||||||
|
else if (rawType === "schoolprogress") return 1;
|
||||||
|
else if (rawType === "schoolopinion") return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decayFor(ts, nowMs) {
|
||||||
|
const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity;
|
||||||
|
return ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreFromActions(actions) {
|
||||||
|
let score = 0;
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const perTypeDay = new Map();
|
||||||
|
for (const action of actions) {
|
||||||
|
const pts = basePointsFor(action);
|
||||||
|
if (!pts) continue;
|
||||||
|
const ts = action.value?.timestamp;
|
||||||
|
const dayKey = `${normalizeType(action)}:${ts ? new Date(ts).toISOString().slice(0, 10) : "?"}`;
|
||||||
|
const n = (perTypeDay.get(dayKey) || 0) + 1;
|
||||||
|
perTypeDay.set(dayKey, n);
|
||||||
|
score += pts * decayFor(ts, nowMs) / n;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.round(Math.min(OWN_SCORE_CAP, score)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function receivedBreakdownFor(userId, actions) {
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const mine = new Set();
|
||||||
|
const firstTs = new Map();
|
||||||
|
const follows = new Map();
|
||||||
|
for (const a of actions) {
|
||||||
|
if (!a.author) continue;
|
||||||
|
const ts = Number(a.receivedAt) || 0;
|
||||||
|
if (ts && (!firstTs.has(a.author) || ts < firstTs.get(a.author))) firstTs.set(a.author, ts);
|
||||||
|
if (a.author === userId && a.id) mine.add(a.id);
|
||||||
|
const c = a.content || {};
|
||||||
|
if (c.type === "contact" && typeof c.contact === "string" && c.contact !== a.author) {
|
||||||
|
const k = `${a.author}|${c.contact}`;
|
||||||
|
const at = Number(a.value?.timestamp) || ts;
|
||||||
|
const prev = follows.get(k);
|
||||||
|
if (!prev || at >= prev.at) follows.set(k, { on: c.following === true && c.blocking !== true, at });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const followersOf = new Map();
|
||||||
|
for (const [k, f] of follows) {
|
||||||
|
if (!f.on) continue;
|
||||||
|
const [from, to] = k.split("|");
|
||||||
|
if (!followersOf.has(to)) followersOf.set(to, new Set());
|
||||||
|
followersOf.get(to).add(from);
|
||||||
|
}
|
||||||
|
const known = (id) => firstTs.has(id) && nowMs - firstTs.get(id) >= RECEIVED_AUTHOR_MIN_AGE_MS;
|
||||||
|
const trusted = (voter, sameSide) => {
|
||||||
|
let n = 0;
|
||||||
|
for (const f of followersOf.get(voter) || []) {
|
||||||
|
if (f === userId || sameSide.has(f) || !known(f)) continue;
|
||||||
|
if (++n >= TRUST_MIN_FOLLOWERS) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const seen = new Set();
|
||||||
|
const perTarget = new Map();
|
||||||
|
const negatives = new Map();
|
||||||
|
const votes = new Map();
|
||||||
|
const reactions = new Map();
|
||||||
|
const credits = [];
|
||||||
|
let positive = 0;
|
||||||
|
let negative = 0;
|
||||||
|
const add = (author, kind, target, pts, ts, capPerTarget = Infinity) => {
|
||||||
|
const k = `${kind}:${author}:${target}`;
|
||||||
|
if (seen.has(k)) return;
|
||||||
|
const n = perTarget.get(`${kind}:${target}`) || 0;
|
||||||
|
if (n >= capPerTarget) return;
|
||||||
|
seen.add(k);
|
||||||
|
perTarget.set(`${kind}:${target}`, n + 1);
|
||||||
|
credits.push({ voter: author, pts, ts: Number(ts) || 0 });
|
||||||
|
};
|
||||||
|
const addNegative = (author, target, pts, ts) => {
|
||||||
|
const k = `neg:${author}:${target}`;
|
||||||
|
if (seen.has(k)) return;
|
||||||
|
seen.add(k);
|
||||||
|
if (!negatives.has(target)) negatives.set(target, []);
|
||||||
|
negatives.get(target).push({ voter: author, pts, ts: Number(ts) || 0 });
|
||||||
|
};
|
||||||
|
for (const a of actions) {
|
||||||
|
const b = a.author;
|
||||||
|
if (!b || b === userId) continue;
|
||||||
|
if (!firstTs.has(b) || nowMs - firstTs.get(b) < RECEIVED_AUTHOR_MIN_AGE_MS) continue;
|
||||||
|
const c = a.content || {};
|
||||||
|
const type = String(c.type || "");
|
||||||
|
const ts = a.value?.timestamp;
|
||||||
|
if (/Opinion$/.test(type) && type !== "pollOpinion" && mine.has(c.target)) {
|
||||||
|
const cat = String(c.category || "");
|
||||||
|
if (NEUTRAL_OPINIONS.has(cat)) continue;
|
||||||
|
if (MODERATION_OPINIONS.has(cat)) addNegative(b, c.target, -5, ts);
|
||||||
|
else if (CONSTRUCTIVE_OPINIONS.has(cat)) addNegative(b, c.target, -3, ts);
|
||||||
|
else add(b, "opinion", c.target, 3, ts);
|
||||||
|
}
|
||||||
|
else if (type === "vote" && c.vote && mine.has(c.vote.link)) {
|
||||||
|
const k = `${b}:${c.vote.link}`;
|
||||||
|
const prev = votes.get(k);
|
||||||
|
if (!prev || (Number(ts) || 0) >= prev.ts) votes.set(k, { author: b, target: c.vote.link, value: Number(c.vote.value) || 0, ts: Number(ts) || 0 });
|
||||||
|
}
|
||||||
|
else if (type === "feed-action" && c.action === "vote" && mine.has(c.root || c.target)) add(b, "opinion", c.root || c.target, 3, ts);
|
||||||
|
else if (type === "feed-action" && c.action === "comment" && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10);
|
||||||
|
else if (type === "feed-action" && c.action === "refeed" && mine.has(c.root)) add(b, "refeed", c.root, 2, ts, 10);
|
||||||
|
else if (type === "post" && c.root && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10);
|
||||||
|
else if (type === "contact" && c.following === true && c.contact === userId) add(b, "follow", userId, 2, ts);
|
||||||
|
else if (type === "chatReaction" && mine.has(c.target)) {
|
||||||
|
const k = `${b}:${c.target}:${c.emoji}`;
|
||||||
|
const prev = reactions.get(k);
|
||||||
|
if (!prev || (Number(ts) || 0) >= prev.ts) reactions.set(k, { author: b, target: c.target, emoji: c.emoji, on: c.on !== false, ts: Number(ts) || 0 });
|
||||||
|
}
|
||||||
|
else if (type === "podcastPlay" && mine.has(c.target)) add(b, "play", c.target, 1, ts, PLAYS_PER_EPISODE_CAP);
|
||||||
|
else if (type === "marketPurchase" && mine.has(c.target)) add(b, "purchase", c.target, 5, ts);
|
||||||
|
else if (type === "shop-purchase" && c.seller === userId) add(b, "purchase", c.productId || a.id, 5, ts);
|
||||||
|
else if (type === "schoolEnroll" && c.value === true && mine.has(c.courseId)) add(b, "enroll", c.courseId, 5, ts);
|
||||||
|
else if (type === "transferConfirm" && mine.has(c.target)) add(b, "confirm", c.target, 3, ts);
|
||||||
|
}
|
||||||
|
for (const r of reactions.values()) {
|
||||||
|
if (!r.on) continue;
|
||||||
|
if (r.emoji === "down") addNegative(r.author, r.target, -3, r.ts);
|
||||||
|
else add(r.author, "reaction", r.target, 1, r.ts, REACTIONS_PER_MESSAGE_CAP);
|
||||||
|
}
|
||||||
|
for (const v of votes.values()) {
|
||||||
|
if (v.value > 0) add(v.author, "opinion", v.target, 3, v.ts);
|
||||||
|
else if (v.value < 0) addNegative(v.author, v.target, -3, v.ts);
|
||||||
|
}
|
||||||
|
const vouched = new Map();
|
||||||
|
const isTrusted = (voter, side, cache) => {
|
||||||
|
if (!cache.has(voter)) cache.set(voter, trusted(voter, side));
|
||||||
|
return cache.get(voter);
|
||||||
|
};
|
||||||
|
const usedByVoter = new Map();
|
||||||
|
for (const c of credits.sort((x, y) => x.ts - y.ts)) {
|
||||||
|
if (!isTrusted(c.voter, new Set(), vouched)) continue;
|
||||||
|
const k = `${c.voter}:${Math.floor(c.ts / KARMA_PERIOD_MS)}`;
|
||||||
|
const used = usedByVoter.get(k) || 0;
|
||||||
|
const take = Math.min(c.pts, POSITIVE_PER_VOTER_CAP - used);
|
||||||
|
if (take <= 0) continue;
|
||||||
|
usedByVoter.set(k, used + take);
|
||||||
|
positive += take * decayFor(c.ts, nowMs);
|
||||||
|
}
|
||||||
|
const blaming = new Set();
|
||||||
|
for (const list of negatives.values()) for (const n of list) blaming.add(n.voter);
|
||||||
|
const vouchedAgainst = new Map();
|
||||||
|
const pattern = new Map();
|
||||||
|
for (const all of negatives.values()) {
|
||||||
|
const list = all.filter(n => isTrusted(n.voter, blaming, vouchedAgainst));
|
||||||
|
if (list.length >= NEGATIVE_MIN_VOTERS) {
|
||||||
|
for (const n of list) negative += Math.abs(n.pts) * decayFor(n.ts, nowMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const n of list) {
|
||||||
|
if (!n.ts || nowMs - n.ts > KARMA_PERIOD_MS) continue;
|
||||||
|
if ((pattern.get(n.voter) || 0) < n.ts) pattern.set(n.voter, n.ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pattern.size >= NEGATIVE_PATTERN_MIN_VOTERS) {
|
||||||
|
for (const ts of pattern.values()) negative += NEGATIVE_PATTERN_POINTS * decayFor(ts, nowMs);
|
||||||
|
}
|
||||||
|
positive = Math.round(positive);
|
||||||
|
negative = Math.round(negative);
|
||||||
|
return { score: positive - negative, positive, negative };
|
||||||
|
}
|
||||||
|
|
||||||
|
function receivedScoreFor(userId, actions) {
|
||||||
|
return receivedBreakdownFor(userId, actions).score;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ({ services, ranges } = {}) => {
|
||||||
|
const normalizeRange = ranges && ranges.normalizeRange ? ranges.normalizeRange : (r) => String(r || "all");
|
||||||
|
const widenRange = ranges && ranges.widenRange ? ranges.widenRange : (arr, r) => normalizeRange(r);
|
||||||
|
const filterByRange = ranges && ranges.filterByRange ? ranges.filterByRange : (arr) => arr;
|
||||||
|
|
||||||
|
let ssbInstance;
|
||||||
|
async function openSsb() {
|
||||||
|
if (ssbInstance) return ssbInstance;
|
||||||
|
if (services?.cooler?.open) ssbInstance = await services.cooler.open();
|
||||||
|
else if (global.ssb) ssbInstance = global.ssb;
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const srv = require("../server/SSB_server.js");
|
||||||
|
ssbInstance = srv?.ssb || srv?.server || srv?.default || null;
|
||||||
|
} catch (_) {
|
||||||
|
ssbInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ssbInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionsCache = null;
|
||||||
|
async function listAllActions() {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const ownSeq = await ownFeedSeq(ssb);
|
||||||
|
if (actionsCache && actionsCache.ownSeq === ownSeq && Date.now() - actionsCache.ts < ACTIONS_CACHE_TTL_MS) return actionsCache.value;
|
||||||
|
const msgs = await readTyped(ssb, CONTENT_TYPES, { limit: Math.max(getLogLimit(), 20000) });
|
||||||
|
const value = msgs.map(m => {
|
||||||
|
const v = m.value || {};
|
||||||
|
const c = v.content || {};
|
||||||
|
return {
|
||||||
|
id: v.key || m.key,
|
||||||
|
author: v.author,
|
||||||
|
type: (c.type || "").toLowerCase(),
|
||||||
|
value: v,
|
||||||
|
content: c,
|
||||||
|
receivedAt: Number(m.timestamp) || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
actionsCache = { ts: Date.now(), ownSeq, value };
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ownFeedSeq(ssb) {
|
||||||
|
return new Promise((res) => {
|
||||||
|
try {
|
||||||
|
pull(
|
||||||
|
ssb.createUserStream({ id: ssb.id, reverse: true, limit: 1 }),
|
||||||
|
pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0))
|
||||||
|
);
|
||||||
|
} catch (_) { res(0); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishKarmaScore(userId, karmaScore) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb || !ssb.publish) return false;
|
||||||
|
if (!(await ownFeedSeq(ssb))) return false;
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const content = { type: "karmaScore", karmaScore, userId, timestamp };
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserActions(userId) {
|
||||||
|
const me = resolveUserId(userId);
|
||||||
|
const actions = await listAllActions();
|
||||||
|
const authored = actions.filter(a =>
|
||||||
|
(a.author && a.author === me) || (a.value?.author && a.value.author === me)
|
||||||
|
);
|
||||||
|
if (authored.length) return authored;
|
||||||
|
return actions.filter(a => {
|
||||||
|
const c = a.content || {};
|
||||||
|
const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
|
||||||
|
return fields.some(f => f && f === me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCarbonGramsForUser(userId) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb || !userId) return 0;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let bytes = 0;
|
||||||
|
pull(
|
||||||
|
ssb.createUserStream({ id: userId }),
|
||||||
|
pull.drain(
|
||||||
|
(m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} },
|
||||||
|
() => resolve((bytes / (1024 * 1024)) * 0.095)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserEngagementScore(userId) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
const uid = resolveUserId(userId);
|
||||||
|
const actions = await fetchUserActions(uid);
|
||||||
|
const all = await listAllActions();
|
||||||
|
const rawKarma = scoreFromActions(actions) + receivedScoreFor(uid, all);
|
||||||
|
const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0);
|
||||||
|
const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams));
|
||||||
|
|
||||||
|
const prev = await getLastKarmaScore(uid);
|
||||||
|
const lastPublishedTimestamp = await getLastPublishedTimestamp(uid);
|
||||||
|
|
||||||
|
const isSelf = idsEqual(uid, ssb.id);
|
||||||
|
const hasSSB = !!(ssb && ssb.publish);
|
||||||
|
|
||||||
|
const changed = (prev === null) || (karmaScore !== prev);
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0;
|
||||||
|
const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
if (isSelf && hasSSB && changed && cooldownOk) {
|
||||||
|
await publishKarmaScore(uid, karmaScore);
|
||||||
|
}
|
||||||
|
return karmaScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLastKarmaScore(userId) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const source = ssb.messagesByType
|
||||||
|
? ssb.messagesByType({ type: "karmaScore", reverse: true })
|
||||||
|
: ssb.createLogStream && ssb.createLogStream({ reverse: true });
|
||||||
|
if (!source) return resolve(null);
|
||||||
|
pull(
|
||||||
|
source,
|
||||||
|
pull.filter(msg => {
|
||||||
|
const v = msg.value || msg;
|
||||||
|
const c = v.content || {};
|
||||||
|
return c && c.type === "karmaScore" && c.userId === userId;
|
||||||
|
}),
|
||||||
|
pull.take(1),
|
||||||
|
pull.collect((err, arr) => {
|
||||||
|
if (err || !arr || !arr.length) return resolve(null);
|
||||||
|
const v = arr[0].value || arr[0];
|
||||||
|
const c = v.content || {};
|
||||||
|
resolve(Number(c.karmaScore) || 0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLastPublishedTimestamp(userId) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb) return new Date(0).toISOString();
|
||||||
|
const fallback = new Date(0).toISOString();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const source = ssb.messagesByType
|
||||||
|
? ssb.messagesByType({ type: "karmaScore", reverse: true })
|
||||||
|
: ssb.createLogStream && ssb.createLogStream({ reverse: true });
|
||||||
|
if (!source) return resolve(fallback);
|
||||||
|
pull(
|
||||||
|
source,
|
||||||
|
pull.filter(msg => {
|
||||||
|
const v = msg.value || msg;
|
||||||
|
const c = v.content || {};
|
||||||
|
return c && c.type === "karmaScore" && c.userId === userId;
|
||||||
|
}),
|
||||||
|
pull.take(1),
|
||||||
|
pull.collect((err, arr) => {
|
||||||
|
if (err || !arr || !arr.length) return resolve(fallback);
|
||||||
|
const v = arr[0].value || arr[0];
|
||||||
|
const c = v.content || {};
|
||||||
|
resolve(c.timestamp || fallback);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getKarmaBreakdown(userId) {
|
||||||
|
const uid = resolveUserId(userId);
|
||||||
|
const received = receivedBreakdownFor(uid, await listAllActions());
|
||||||
|
return { karmaScore: await getUserEngagementScore(uid), positive: received.positive, negative: received.negative };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listKarmaHistory(userId, range = "all", explicit = true) {
|
||||||
|
const ssb = await openSsb();
|
||||||
|
if (!ssb) return { points: [], hasAny: false, range: normalizeRange(range) };
|
||||||
|
const uid = resolveUserId(userId);
|
||||||
|
const msgs = await readTyped(ssb, ["karmaScore"], { limit: Math.max(getLogLimit(), 5000) });
|
||||||
|
const all = msgs
|
||||||
|
.filter(m => m.value && m.value.author === uid && m.value.content && m.value.content.type === "karmaScore")
|
||||||
|
.map(m => ({ ts: Date.parse(m.value.content.timestamp || "") || Number(m.value.timestamp) || 0, score: Number(m.value.content.karmaScore) || 0 }))
|
||||||
|
.filter(x => x.ts > 0)
|
||||||
|
.sort((a, b) => a.ts - b.ts);
|
||||||
|
const effective = explicit ? normalizeRange(range) : widenRange(all, range);
|
||||||
|
return { points: filterByRange(all, effective).slice(-120), hasAny: all.length >= 2, range: effective };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
listAllActions,
|
||||||
|
fetchUserActions,
|
||||||
|
scoreFromActions,
|
||||||
|
receivedBreakdownFor,
|
||||||
|
receivedScoreFor,
|
||||||
|
getCarbonGramsForUser,
|
||||||
|
getUserEngagementScore,
|
||||||
|
getKarmaBreakdown,
|
||||||
|
getLastKarmaScore,
|
||||||
|
getLastPublishedTimestamp,
|
||||||
|
publishKarmaScore,
|
||||||
|
listKarmaHistory,
|
||||||
|
basePointsFor,
|
||||||
|
decayFor,
|
||||||
|
NEGATIVE_MIN_VOTERS,
|
||||||
|
NEGATIVE_PATTERN_MIN_VOTERS,
|
||||||
|
NEGATIVE_PATTERN_POINTS,
|
||||||
|
POSITIVE_PER_VOTER_CAP,
|
||||||
|
KARMA_PERIOD_MS,
|
||||||
|
TRUST_MIN_FOLLOWERS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -624,6 +624,12 @@ models.blob = {
|
||||||
};
|
};
|
||||||
|
|
||||||
models.friend = {
|
models.friend = {
|
||||||
|
blockedIds: async () => {
|
||||||
|
const ssb = await cooler.open();
|
||||||
|
const graph = await new Promise((resolve) => ssb.friends.graph((err, g) => resolve(err || !g ? {} : g)));
|
||||||
|
const mine = graph[ssb.id] || {};
|
||||||
|
return new Set(Object.keys(mine).filter(feed => Math.round(mine[feed]) === -1));
|
||||||
|
},
|
||||||
setRelationship: async ({ feedId, following, blocking }) => {
|
setRelationship: async ({ feedId, following, blocking }) => {
|
||||||
if (following && blocking) {
|
if (following && blocking) {
|
||||||
throw new Error("Cannot follow and block at the same time");
|
throw new Error("Cannot follow and block at the same time");
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ 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');
|
||||||
const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log');
|
const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log');
|
||||||
|
const longText = require('../backend/long_text');
|
||||||
|
const isRootBlogPost = (c) =>
|
||||||
|
!!c && c.type === 'post' && typeof c.text === 'string' &&
|
||||||
|
!c.root && !c.fork && !c.branch && !c.about && !c.target &&
|
||||||
|
c.private !== true && !Array.isArray(c.recps);
|
||||||
|
|
||||||
const { buildVoteTally } = require('../backend/vote_tally');
|
const { buildVoteTally } = require('../backend/vote_tally');
|
||||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
|
|
@ -24,13 +30,14 @@ module.exports = ({ cooler }) => {
|
||||||
|
|
||||||
const validTypes = [
|
const validTypes = [
|
||||||
'bookmark', 'votes', 'transfer',
|
'bookmark', 'votes', 'transfer',
|
||||||
'feed', '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'
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPreview = c => {
|
const getPreview = c => {
|
||||||
if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`;
|
if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`;
|
||||||
|
if (c.type === 'post') return c.contentWarning || c.text || '';
|
||||||
return c.text || c.description || c.title || c.name || '';
|
return c.text || c.description || c.title || c.name || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -83,6 +90,18 @@ module.exports = ({ cooler }) => {
|
||||||
if (c.type === 'feed' && !byId.has(key)) {
|
if (c.type === 'feed' && !byId.has(key)) {
|
||||||
byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } });
|
byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } });
|
||||||
}
|
}
|
||||||
|
if (isRootBlogPost(c) && !byId.has(key)) {
|
||||||
|
byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkLookup = longText.lookupIn(longText.indexChunks(messages));
|
||||||
|
for (const [key, m] of Array.from(byId.entries())) {
|
||||||
|
const c = m.value?.content;
|
||||||
|
if (c?.type === 'post' && longText.hasChunks(c)) {
|
||||||
|
const content = longText.resolveField(c, 'text', m.value.author, chunkLookup);
|
||||||
|
byId.set(key, { key, value: { ...m.value, content, preview: getPreview(content) } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const vg = versionGroups(Array.from(byId.values()).map(versionEntry));
|
const vg = versionGroups(Array.from(byId.values()).map(versionEntry));
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ const isDead = ({ key, failures = 0, now = Date.now() } = {}) => {
|
||||||
return Math.max(observed, Number(failures) * RETRY_CAP_MS) >= DEAD_AFTER_MS;
|
return Math.max(observed, Number(failures) * RETRY_CAP_MS) >= DEAD_AFTER_MS;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const failingSince = (key) => Number((load()[canonicalKey(key)] || {}).failingSince) || 0;
|
||||||
|
|
||||||
const addressOf = (item) => {
|
const addressOf = (item) => {
|
||||||
const m = Array.isArray(item) ? String(item[0] || '').match(/^(?:net|onion):([^:~]+):(\d+)/) : null;
|
const m = Array.isArray(item) ? String(item[0] || '').match(/^(?:net|onion):([^:~]+):(\d+)/) : null;
|
||||||
if (m) return `${m[1]}:${m[2]}`;
|
if (m) return `${m[1]}:${m[2]}`;
|
||||||
|
|
@ -100,4 +102,4 @@ const NET_REASON_KEYS = {
|
||||||
notfound: 'peerErrNotFound', unreachable: 'peerErrUnreachable', timeout: 'peerErrTimeout', dropped: 'peerErrDropped', other: 'peerErrOther', silent: 'peerErrSilent'
|
notfound: 'peerErrNotFound', unreachable: 'peerErrUnreachable', timeout: 'peerErrTimeout', dropped: 'peerErrDropped', other: 'peerErrOther', silent: 'peerErrSilent'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { canonicalKey, observe, isDead, failuresByKey, classifyNetError, NET_REASON_KEYS, DEAD_AFTER_MS };
|
module.exports = { canonicalKey, observe, isDead, failingSince, failuresByKey, classifyNetError, NET_REASON_KEYS, DEAD_AFTER_MS };
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,41 @@ const resolvePixels = (messages) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaced = new Set();
|
const replaced = new Set();
|
||||||
for (const { m, coordinateKey } of byKey.values()) {
|
const parentOf = (k) => {
|
||||||
|
const { m, coordinateKey } = byKey.get(k);
|
||||||
const prev = byKey.get(m.value.content.replaces);
|
const prev = byKey.get(m.value.content.replaces);
|
||||||
if (prev && prev.coordinateKey === coordinateKey) replaced.add(prev.m.key);
|
return prev && prev.coordinateKey === coordinateKey ? prev.m.key : null;
|
||||||
|
};
|
||||||
|
for (const k of byKey.keys()) {
|
||||||
|
const parent = parentOf(k);
|
||||||
|
if (parent) replaced.add(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const depthOf = new Map();
|
||||||
|
const depth = (key) => {
|
||||||
|
const chain = [];
|
||||||
|
const inChain = new Set();
|
||||||
|
let k = key;
|
||||||
|
while (k && !depthOf.has(k) && !inChain.has(k)) {
|
||||||
|
chain.push(k);
|
||||||
|
inChain.add(k);
|
||||||
|
k = parentOf(k);
|
||||||
|
}
|
||||||
|
let d = k && depthOf.has(k) ? depthOf.get(k) + 1 : 0;
|
||||||
|
for (let i = chain.length - 1; i >= 0; i--) depthOf.set(chain[i], d++);
|
||||||
|
return depthOf.get(key);
|
||||||
|
};
|
||||||
|
|
||||||
const latest = new Map();
|
const latest = new Map();
|
||||||
for (const [k, { m, coordinateKey }] of byKey) {
|
for (const [k, { m, coordinateKey }] of byKey) {
|
||||||
if (replaced.has(k) || tombstoned.has(k)) continue;
|
if (replaced.has(k) || tombstoned.has(k)) continue;
|
||||||
const cur = latest.get(coordinateKey);
|
const cur = latest.get(coordinateKey);
|
||||||
|
if (!cur) { latest.set(coordinateKey, m); continue; }
|
||||||
|
const d = depth(k);
|
||||||
|
const curD = depth(cur.key);
|
||||||
const ts = m.value.timestamp || 0;
|
const ts = m.value.timestamp || 0;
|
||||||
const curTs = cur ? cur.value.timestamp || 0 : 0;
|
const curTs = cur.value.timestamp || 0;
|
||||||
if (!cur || ts > curTs || (ts === curTs && k > cur.key)) latest.set(coordinateKey, m);
|
if (d > curD || (d === curD && (ts > curTs || (ts === curTs && k > cur.key)))) latest.set(coordinateKey, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
return latest;
|
return latest;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ const normalizeTags = (raw) => {
|
||||||
return String(raw).split(",").map(t => t.trim()).filter(Boolean)
|
return String(raw).split(",").map(t => t.trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
const normalizeStatus = (v) => (STATUSES.includes(String(v || "").toUpperCase()) ? String(v).toUpperCase() : "OPEN")
|
const normalizeStatus = (v) => (STATUSES.includes(String(v || "").toUpperCase()) ? String(v).toUpperCase() : "OPEN")
|
||||||
const blobRef = (v) => (BLOB_ID.test(safeText(v)) ? safeText(v) : "")
|
const SEALED_REF = /^\/blob\/sealed\/%26[A-Za-z0-9%]+\.sha256\?k=[0-9a-f]{64}&t=[^\s"<>]*$/
|
||||||
|
const blobRef = (v) => (BLOB_ID.test(safeText(v)) || SEALED_REF.test(safeText(v)) ? safeText(v) : "")
|
||||||
const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v)))
|
const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v)))
|
||||||
const rpcValue = (fn, ...args) => new Promise((resolve) => {
|
const rpcValue = (fn, ...args) => new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -352,6 +353,8 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
return { title: "", description: "", image: "", tags: [], rid: "", _undec: true }
|
return { title: "", description: "", image: "", tags: [], rid: "", _undec: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomTypeOf = (c) => c.status === "INVITE-ONLY" || (c.status === "CLOSED" && c.encrypted === true && !c.tribeId) ? "INVITE-ONLY" : "OPEN"
|
||||||
|
|
||||||
const buildRoom = (node, rootId, tribeKeys, members) => {
|
const buildRoom = (node, rootId, tribeKeys, members) => {
|
||||||
const c = node.c || {}
|
const c = node.c || {}
|
||||||
if (c.type !== "room") return null
|
if (c.type !== "room") return null
|
||||||
|
|
@ -368,7 +371,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
image: f.image,
|
image: f.image,
|
||||||
tags: f.tags,
|
tags: f.tags,
|
||||||
status: ["OPEN", "INVITE-ONLY", "CLOSED"].includes(c.status) ? c.status : "OPEN",
|
status: ["OPEN", "INVITE-ONLY", "CLOSED"].includes(c.status) ? c.status : "OPEN",
|
||||||
type: c.status === "INVITE-ONLY" || (c.status === "CLOSED" && c.encrypted === true && !c.tribeId) ? "INVITE-ONLY" : "OPEN",
|
type: roomTypeOf(c),
|
||||||
author,
|
author,
|
||||||
members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []),
|
members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []),
|
||||||
invites: Array.isArray(c.invites) ? c.invites : [],
|
invites: Array.isArray(c.invites) ? c.invites : [],
|
||||||
|
|
@ -555,6 +558,10 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
return republish(id, (c) => ({ ...c, status: "CLOSED" }))
|
return republish(id, (c) => ({ ...c, status: "CLOSED" }))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async reopenRoomById(id) {
|
||||||
|
return republish(id, (c) => ({ ...c, status: roomTypeOf(c) }))
|
||||||
|
},
|
||||||
|
|
||||||
async setRoomLine(id, line) {
|
async setRoomLine(id, line) {
|
||||||
return republish(id, (c) => ({ ...c, line: line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" }))
|
return republish(id, (c) => ({ ...c, line: line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" }))
|
||||||
},
|
},
|
||||||
|
|
@ -782,6 +789,38 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => {
|
||||||
return asP(ph.camera, !!flag)
|
return asP(ph.camera, !!flag)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async stream(opts) {
|
||||||
|
const ph = await phone()
|
||||||
|
if (!ph || typeof ph.stream !== "function") return null
|
||||||
|
return asP(ph.stream, opts || {})
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewerJoin(room) {
|
||||||
|
const ph = await phone()
|
||||||
|
if (!ph || typeof ph.viewerJoin !== "function") return null
|
||||||
|
const params = await this.liveParams(room)
|
||||||
|
if (!params || params.secrets) return null
|
||||||
|
return asP(ph.viewerJoin, params)
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewerLeave(rid) {
|
||||||
|
const ph = await phone()
|
||||||
|
if (!ph || typeof ph.viewerLeave !== "function") return null
|
||||||
|
return asP(ph.viewerLeave, { rid: String(rid || "") })
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewerRooms() {
|
||||||
|
const ph = await phone()
|
||||||
|
if (!ph || typeof ph.viewerRooms !== "function") return []
|
||||||
|
const list = await rpcValue(ph.viewerRooms)
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
},
|
||||||
|
|
||||||
|
async publicFeed(rid, kind) {
|
||||||
|
const ph = await phone()
|
||||||
|
return ph && typeof ph.publicFeed === "function" ? ph.publicFeed({ rid: String(rid || ""), kind: kind === "sound" ? "sound" : "grid" }) : null
|
||||||
|
},
|
||||||
|
|
||||||
async recStart() {
|
async recStart() {
|
||||||
const ph = await phone()
|
const ph = await phone()
|
||||||
if (!ph || typeof ph.roomRecStart !== "function") return null
|
if (!ph || typeof ph.roomRecStart !== "function") return null
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ 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 { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
|
const opinionCategories = require('../backend/opinion_categories');
|
||||||
|
const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive);
|
||||||
|
const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation);
|
||||||
|
const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral);
|
||||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
const DISK_USAGE_TTL_MS = 10 * 60 * 1000;
|
const DISK_USAGE_TTL_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
|
@ -223,8 +227,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||||
let s = 0;
|
let s = 0;
|
||||||
for (const k in cats) {
|
for (const k in cats) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
|
if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
|
||||||
|
if (NEUTRAL_OPINIONS.has(k)) continue;
|
||||||
if (k === 'interesting' || k === 'inspiring') s += 5;
|
if (k === 'interesting' || k === 'inspiring') s += 5;
|
||||||
else if (k === 'boring' || k === 'spam' || k === 'propaganda') s -= 3;
|
else if (MODERATION_OPINIONS.has(k)) s -= 5;
|
||||||
|
else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3;
|
||||||
else s += 1;
|
else s += 1;
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async listTags(filter = 'all', search = '') {
|
async listTags(filter = 'all', search = '', opts = {}) {
|
||||||
const ssbClient = await openSsb();
|
const ssbClient = await openSsb();
|
||||||
|
const hiddenAuthors = opts.hiddenAuthors instanceof Set ? opts.hiddenAuthors : null;
|
||||||
|
const hiddenIds = opts.hiddenIds instanceof Set ? opts.hiddenIds : null;
|
||||||
|
|
||||||
const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true });
|
const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true });
|
||||||
|
|
||||||
|
|
@ -140,6 +142,8 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
||||||
let filtered = Array.from(latestByKey.values()).filter(msg => {
|
let filtered = Array.from(latestByKey.values()).filter(msg => {
|
||||||
const c = msg?.value?.content;
|
const c = msg?.value?.content;
|
||||||
if (!c || c.type === 'tombstone') return false;
|
if (!c || c.type === 'tombstone') return false;
|
||||||
|
if (hiddenAuthors && hiddenAuthors.has(String(msg?.value?.author || ''))) return false;
|
||||||
|
if (hiddenIds && hiddenIds.has(msg.key)) return false;
|
||||||
if (tombstoned.has(msg.key)) return false;
|
if (tombstoned.has(msg.key)) return false;
|
||||||
if (c.encryptedPayload) return false;
|
if (c.encryptedPayload) return false;
|
||||||
if (!Array.isArray(c.tags) || !c.tags.filter(Boolean).length) return false;
|
if (!Array.isArray(c.tags) || !c.tags.filter(Boolean).length) return false;
|
||||||
|
|
@ -185,6 +189,8 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
||||||
const viewerId = ssbClient2.id;
|
const viewerId = ssbClient2.id;
|
||||||
const pads = await padsModel.listAll({ filter: 'all', viewerId }).catch(() => []);
|
const pads = await padsModel.listAll({ filter: 'all', viewerId }).catch(() => []);
|
||||||
for (const pad of pads) {
|
for (const pad of pads) {
|
||||||
|
if (hiddenAuthors && hiddenAuthors.has(String(pad.author || ''))) continue;
|
||||||
|
if (hiddenIds && (hiddenIds.has(pad.id) || hiddenIds.has(pad.key) || hiddenIds.has(pad.rootId))) continue;
|
||||||
if (pad.status === 'INVITE-ONLY' && pad.author !== viewerId && !(Array.isArray(pad.members) && pad.members.includes(viewerId))) continue;
|
if (pad.status === 'INVITE-ONLY' && pad.author !== viewerId && !(Array.isArray(pad.members) && pad.members.includes(viewerId))) continue;
|
||||||
if (!Array.isArray(pad.tags)) continue;
|
if (!Array.isArray(pad.tags)) continue;
|
||||||
const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean));
|
const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||||
const opinionCategories = require('../backend/opinion_categories');
|
const opinionCategories = require('../backend/opinion_categories');
|
||||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||||
const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log');
|
const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log');
|
||||||
|
const longText = require('../backend/long_text');
|
||||||
|
const isRootBlogPost = (c) =>
|
||||||
|
!!c && c.type === 'post' && typeof c.text === 'string' &&
|
||||||
|
!c.root && !c.fork && !c.branch && !c.about && !c.target &&
|
||||||
|
c.private !== true && !Array.isArray(c.recps);
|
||||||
|
|
||||||
const { buildVoteTally } = require('../backend/vote_tally');
|
const { buildVoteTally } = require('../backend/vote_tally');
|
||||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||||
|
|
||||||
|
|
@ -21,7 +27,7 @@ module.exports = ({ cooler }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const types = [
|
const types = [
|
||||||
'bookmark', 'votes', 'poll', 'feed',
|
'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'
|
||||||
|
|
@ -52,6 +58,15 @@ module.exports = ({ cooler }) => {
|
||||||
if (c.opinions && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m);
|
if (c.opinions && !['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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkLookup = longText.lookupIn(longText.indexChunks(messages));
|
||||||
|
for (const [k, m] of Array.from(itemsById.entries())) {
|
||||||
|
const c = m.value?.content;
|
||||||
|
if (c?.type === 'post' && longText.hasChunks(c)) {
|
||||||
|
itemsById.set(k, { ...m, value: { ...m.value, content: longText.resolveField(c, 'text', m.value.author, chunkLookup) } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const vg = versionGroups(Array.from(itemsById.values()).map(versionEntry));
|
const vg = versionGroups(Array.from(itemsById.values()).map(versionEntry));
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const longText = require('../backend/long_text');
|
||||||
const { versionGroups } = require('../backend/dedupe');
|
const { versionGroups } = require('../backend/dedupe');
|
||||||
const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite-tombstone', 'tombstone', 'contact'];
|
const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite-tombstone', 'tombstone', 'contact'];
|
||||||
|
|
||||||
const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed', 'pixelia'];
|
const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed'];
|
||||||
const categories = require('../backend/opinion_categories');
|
const categories = require('../backend/opinion_categories');
|
||||||
const VALID_STATUSES = ['OPEN', 'CLOSED', 'IN-PROGRESS'];
|
const VALID_STATUSES = ['OPEN', 'CLOSED', 'IN-PROGRESS'];
|
||||||
const VALID_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
const VALID_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,74 @@ const loadState = () => {
|
||||||
};
|
};
|
||||||
const saveState = (s) => writeJson(statePath, s);
|
const saveState = (s) => writeJson(statePath, s);
|
||||||
|
|
||||||
|
const hiddenPath = require('../configs/state-manager').statePath('viewer_hidden.json');
|
||||||
|
let hiddenState = null;
|
||||||
|
const loadHidden = () => {
|
||||||
|
if (!hiddenState) {
|
||||||
|
const raw = readJson(hiddenPath, {});
|
||||||
|
const muted = Array.isArray(raw && raw.muted) ? raw.muted : [];
|
||||||
|
const hidden = Array.isArray(raw && raw.hidden) ? raw.hidden : [];
|
||||||
|
hiddenState = {
|
||||||
|
muted: muted.map(x => (typeof x === 'string' ? { id: x, at: 0 } : x)).filter(x => x && typeof x.id === 'string'),
|
||||||
|
hidden: hidden.filter(x => x && typeof x.id === 'string')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return hiddenState;
|
||||||
|
};
|
||||||
|
const saveHidden = () => writeJson(hiddenPath, loadHidden());
|
||||||
|
|
||||||
|
const mutedList = () => loadHidden().muted.slice();
|
||||||
|
const mutedIds = () => new Set(loadHidden().muted.map(x => x.id));
|
||||||
|
const isMuted = (id) => !!id && loadHidden().muted.some(x => x.id === id);
|
||||||
|
const mute = (id) => {
|
||||||
|
const s = loadHidden();
|
||||||
|
if (!id || s.muted.some(x => x.id === id)) return false;
|
||||||
|
s.muted.push({ id, at: Date.now() });
|
||||||
|
saveHidden();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const unmute = (id) => {
|
||||||
|
const s = loadHidden();
|
||||||
|
const before = s.muted.length;
|
||||||
|
s.muted = s.muted.filter(x => x.id !== id);
|
||||||
|
if (s.muted.length !== before) saveHidden();
|
||||||
|
return s.muted.length !== before;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hiddenList = () => loadHidden().hidden.slice();
|
||||||
|
const hiddenIds = () => new Set(loadHidden().hidden.map(x => x.id));
|
||||||
|
const isHidden = (id) => !!id && loadHidden().hidden.some(x => x.id === id);
|
||||||
|
const hideContent = ({ id, href = '', label = '' } = {}) => {
|
||||||
|
const s = loadHidden();
|
||||||
|
if (!id || s.hidden.some(x => x.id === id)) return false;
|
||||||
|
s.hidden.push({ id, href: String(href || '').slice(0, 300), label: String(label || '').slice(0, 120), at: Date.now() });
|
||||||
|
saveHidden();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const unhideContent = (id) => {
|
||||||
|
const s = loadHidden();
|
||||||
|
const before = s.hidden.length;
|
||||||
|
s.hidden = s.hidden.filter(x => x.id !== id);
|
||||||
|
if (s.hidden.length !== before) saveHidden();
|
||||||
|
return s.hidden.length !== before;
|
||||||
|
};
|
||||||
|
|
||||||
|
const idsOfItem = (item) => {
|
||||||
|
if (!item || typeof item !== 'object') return [];
|
||||||
|
return [item.id, item.key, item.rootId, item.favId].filter(v => typeof v === 'string');
|
||||||
|
};
|
||||||
|
const isItemHidden = (item, authorOf, { authors, ids } = {}) => {
|
||||||
|
if (!item) return false;
|
||||||
|
const a = typeof authorOf === 'function' ? authorOf(item) : null;
|
||||||
|
if (a && authors && authors.has(String(a))) return true;
|
||||||
|
if (ids && ids.size && idsOfItem(item).some(id => ids.has(id))) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const lanPeersPath = require('../configs/state-manager').statePath('lan-peers.json');
|
const lanPeersPath = require('../configs/state-manager').statePath('lan-peers.json');
|
||||||
const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/;
|
const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/;
|
||||||
let lanKnown = null;
|
const LAN_SEEN_STEP_MS = 60 * 1000;
|
||||||
|
let lanSeen = null;
|
||||||
|
|
||||||
const canonicalFeedId = (key) => {
|
const canonicalFeedId = (key) => {
|
||||||
let k = String(key || '').trim();
|
let k = String(key || '').trim();
|
||||||
|
|
@ -62,22 +127,41 @@ const lanKeysFromConn = (entries) => {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lanPeers = () => {
|
const lanPeersSeen = () => {
|
||||||
if (!lanKnown) {
|
if (!lanSeen) {
|
||||||
const list = readJson(lanPeersPath, []);
|
const list = readJson(lanPeersPath, []);
|
||||||
lanKnown = new Set((Array.isArray(list) ? list : []).map(canonicalFeedId).filter(Boolean));
|
lanSeen = new Map();
|
||||||
|
for (const item of (Array.isArray(list) ? list : [])) {
|
||||||
|
const id = canonicalFeedId(typeof item === 'string' ? item : item && item.key);
|
||||||
|
if (id) lanSeen.set(id, Number(item && item.seenAt) || 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lanKnown;
|
return lanSeen;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rememberLanPeers = (keys) => {
|
const lanPeers = () => new Set(lanPeersSeen().keys());
|
||||||
const set = lanPeers();
|
|
||||||
|
const rememberLanPeers = (keys, now = Date.now()) => {
|
||||||
|
const seen = lanPeersSeen();
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const k of keys || []) {
|
for (const k of keys || []) {
|
||||||
const id = canonicalFeedId(k);
|
const id = canonicalFeedId(k);
|
||||||
if (id && !set.has(id)) { set.add(id); changed = true; }
|
if (!id) continue;
|
||||||
|
const prev = seen.get(id);
|
||||||
|
if (prev === undefined || now - prev >= LAN_SEEN_STEP_MS) { seen.set(id, now); changed = true; }
|
||||||
}
|
}
|
||||||
if (changed) writeJson(lanPeersPath, [...set]);
|
if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt })));
|
||||||
|
return changed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const forgetLanPeers = (keys) => {
|
||||||
|
const seen = lanPeersSeen();
|
||||||
|
let changed = false;
|
||||||
|
for (const k of keys || []) {
|
||||||
|
const id = canonicalFeedId(k);
|
||||||
|
if (id && seen.delete(id)) changed = true;
|
||||||
|
}
|
||||||
|
if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt })));
|
||||||
return changed;
|
return changed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -104,7 +188,8 @@ const filterByWish = async (items, { wish, viewer, authorOf: author, isOwn, isMu
|
||||||
|
|
||||||
const wishMutualsOnly = () => getConfig().wish === 'mutuals';
|
const wishMutualsOnly = () => getConfig().wish === 'mutuals';
|
||||||
const pmMutualsOnly = () => getConfig().pmVisibility === 'mutuals';
|
const pmMutualsOnly = () => getConfig().pmVisibility === 'mutuals';
|
||||||
const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly();
|
const pmFollowingOnly = () => getConfig().pmVisibility === 'following';
|
||||||
|
const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly() || pmFollowingOnly();
|
||||||
|
|
||||||
const listPending = () => loadState().pending;
|
const listPending = () => loadState().pending;
|
||||||
const enqueuePending = (followerId, extra = {}) => {
|
const enqueuePending = (followerId, extra = {}) => {
|
||||||
|
|
@ -183,8 +268,13 @@ const applyMutualSupportFilter = async (items, viewerId, friendModel) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const canSendPmTo = async (viewerId, recipientId, friendModel) => {
|
const canSendPmTo = async (viewerId, recipientId, friendModel) => {
|
||||||
if (!pmMutualsOnly()) return { allowed: true };
|
|
||||||
if (viewerId === recipientId) return { allowed: true };
|
if (viewerId === recipientId) return { allowed: true };
|
||||||
|
if (pmFollowingOnly()) {
|
||||||
|
let rel = null;
|
||||||
|
try { rel = friendModel && typeof friendModel.getRelationship === 'function' ? await friendModel.getRelationship(recipientId) : null; } catch (_) { rel = null; }
|
||||||
|
return rel && rel.following ? { allowed: true } : { allowed: false, reason: 'not-following' };
|
||||||
|
}
|
||||||
|
if (!pmMutualsOnly()) return { allowed: true };
|
||||||
const isMutual = makeMutualCache(friendModel);
|
const isMutual = makeMutualCache(friendModel);
|
||||||
if (await isMutual(recipientId)) return { allowed: true };
|
if (await isMutual(recipientId)) return { allowed: true };
|
||||||
return { allowed: false, reason: 'non-mutual' };
|
return { allowed: false, reason: 'non-mutual' };
|
||||||
|
|
@ -194,6 +284,7 @@ module.exports = {
|
||||||
COOLDOWN_MS,
|
COOLDOWN_MS,
|
||||||
wishMutualsOnly,
|
wishMutualsOnly,
|
||||||
pmMutualsOnly,
|
pmMutualsOnly,
|
||||||
|
pmFollowingOnly,
|
||||||
isFrictionActive,
|
isFrictionActive,
|
||||||
listPending,
|
listPending,
|
||||||
enqueuePending,
|
enqueuePending,
|
||||||
|
|
@ -211,6 +302,19 @@ module.exports = {
|
||||||
isPrivateHost,
|
isPrivateHost,
|
||||||
lanKeysFromConn,
|
lanKeysFromConn,
|
||||||
lanPeers,
|
lanPeers,
|
||||||
|
lanPeersSeen,
|
||||||
rememberLanPeers,
|
rememberLanPeers,
|
||||||
|
forgetLanPeers,
|
||||||
filterByWish,
|
filterByWish,
|
||||||
|
mutedList,
|
||||||
|
mutedIds,
|
||||||
|
isMuted,
|
||||||
|
mute,
|
||||||
|
unmute,
|
||||||
|
hiddenList,
|
||||||
|
hiddenIds,
|
||||||
|
isHidden,
|
||||||
|
hideContent,
|
||||||
|
unhideContent,
|
||||||
|
isItemHidden,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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')); } } 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')); } } catch (_) {} return pm; })())
|
||||||
.use(require('./network_pause'));
|
.use(require('./network_pause'));
|
||||||
|
|
||||||
if (!config.pub) {
|
if (!config.pub) {
|
||||||
|
|
|
||||||
2
src/server/package-lock.json
generated
2
src/server/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.1",
|
"version": "1.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krakenslab/oasis",
|
"name": "@krakenslab/oasis",
|
||||||
"version": "1.3.1",
|
"version": "1.3.3",
|
||||||
"description": "Oasis - Social Networking Utopia",
|
"description": "Oasis - Social Networking Utopia",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const RING_PER_WINDOW = 20;
|
||||||
const SEEN_MAX = 5000;
|
const SEEN_MAX = 5000;
|
||||||
const HUB_ROOMS_MAX = 256;
|
const HUB_ROOMS_MAX = 256;
|
||||||
const HUB_RIDS_PER_ROOM = 4;
|
const HUB_RIDS_PER_ROOM = 4;
|
||||||
const GROUP_MAX = 7;
|
const GROUP_MAX = 10;
|
||||||
const MIX_QUEUE = 10;
|
const MIX_QUEUE = 10;
|
||||||
const ROOM_MAX = 50;
|
const ROOM_MAX = 50;
|
||||||
const ROOM_QUIET_AT = 10;
|
const ROOM_QUIET_AT = 10;
|
||||||
|
|
@ -59,12 +59,18 @@ const K_VIDEO = 4;
|
||||||
const VIDEO_CHUNK = 8 * 1024;
|
const VIDEO_CHUNK = 8 * 1024;
|
||||||
const VIDEO_PAD = 1024;
|
const VIDEO_PAD = 1024;
|
||||||
const VIDEO_HDR = 6;
|
const VIDEO_HDR = 6;
|
||||||
|
const VIDEO_FILM = 0x80;
|
||||||
const VIDEO_FRAME_MAX = 256 * 1024;
|
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 GRID_TILE_LARGE = { w: 640, h: 480 };
|
||||||
|
const GRID_LARGE_MAX = 4;
|
||||||
|
const ROOM_VIDEO_CHUNK_EXTRA = 45;
|
||||||
const VIDEO_HOLD_MS = 3000;
|
const VIDEO_HOLD_MS = 3000;
|
||||||
const ROOM_VIDEO_FRAME_MAX = 10 * 1024;
|
const ROOM_VIDEO_FRAME_MAX = 10 * 1024;
|
||||||
const ROOM_VIDEO_BYTES_PER_S = 200 * 1024;
|
const ROOM_VIDEO_BYTES_PER_S = 200 * 1024;
|
||||||
|
const ROOM_VIDEO_SEND_MAX = Math.floor(ROOM_VIDEO_BYTES_PER_S * 0.9);
|
||||||
const VAD_MIN = 200;
|
const VAD_MIN = 200;
|
||||||
const VAD_FLOOR_MAX = 1500;
|
const VAD_FLOOR_MAX = 1500;
|
||||||
const VAD_HANG = 15;
|
const VAD_HANG = 15;
|
||||||
|
|
@ -215,7 +221,71 @@ const systemVideo = (server, device) => {
|
||||||
};
|
};
|
||||||
let videoFactory = systemVideo;
|
let videoFactory = systemVideo;
|
||||||
|
|
||||||
const videoChunks = (seq, jpeg) => {
|
const hasBin = (bin) => (path.isAbsolute(String(bin || '')) ? fs.existsSync(bin) : onPath(bin));
|
||||||
|
const gstQuote = (s) => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||||
|
const STREAM_BACKENDS = [
|
||||||
|
{
|
||||||
|
bin: () => process.env.OASIS_FFMPEG || 'ffmpeg',
|
||||||
|
retryWithoutAudio: true,
|
||||||
|
args: (file, withAudio) => [
|
||||||
|
'-loglevel', 'quiet', '-protocol_whitelist', 'file,crypto', '-re', '-i', file,
|
||||||
|
'-map', '0:v:0', '-vf', `fps=${VIDEO_FPS},scale=${STREAM_SIZE.w}:${STREAM_SIZE.h}:force_original_aspect_ratio=decrease:flags=lanczos,pad=${STREAM_SIZE.w}:${STREAM_SIZE.h}:(ow-iw)/2:(oh-ih)/2`,
|
||||||
|
'-c:v', 'mjpeg', '-q:v', '9', '-f', 'image2pipe', 'pipe:1',
|
||||||
|
...(withAudio ? ['-map', '0:a:0', '-ac', '1', '-ar', String(RATE), '-f', 's16le', 'pipe:3'] : [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bin: () => process.env.OASIS_GST_LAUNCH || 'gst-launch-1.0',
|
||||||
|
args: (file) => [
|
||||||
|
'-q', 'filesrc', `location=${gstQuote(file)}`, '!', 'decodebin', 'name=d',
|
||||||
|
'd.', '!', 'queue', '!', 'videoconvert', '!', 'videoscale', 'add-borders=true', '!', `video/x-raw,width=${STREAM_SIZE.w},height=${STREAM_SIZE.h},pixel-aspect-ratio=1/1`, '!', 'videorate', '!', `video/x-raw,framerate=${VIDEO_FPS}/1`, '!', 'jpegenc', 'quality=50', '!', 'fdsink', 'fd=1', 'sync=true',
|
||||||
|
'd.', '!', 'queue', '!', 'audioconvert', '!', 'audioresample', '!', `audio/x-raw,format=S16LE,rate=${RATE},channels=1`, '!', 'fdsink', 'fd=3', 'sync=true', 'async=false'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const STREAM_STALL_MS = 5000;
|
||||||
|
const STREAM_START_MS = 20000;
|
||||||
|
const systemStream = () => {
|
||||||
|
const backend = STREAM_BACKENDS.find(b => hasBin(b.bin()));
|
||||||
|
if (!backend) return null;
|
||||||
|
return {
|
||||||
|
open(file, { onJpeg, onPcm, onEnd }) {
|
||||||
|
let proc = null;
|
||||||
|
let frames = 0;
|
||||||
|
let lastAt = 0;
|
||||||
|
let ended = false;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const watch = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (frames ? now - lastAt > STREAM_STALL_MS : now - startedAt > STREAM_START_MS) finish();
|
||||||
|
}, 1000);
|
||||||
|
if (watch.unref) watch.unref();
|
||||||
|
const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} };
|
||||||
|
const finish = () => { if (ended) return; halt(); onEnd(); };
|
||||||
|
const run = (withAudio) => {
|
||||||
|
const p = spawn(backend.bin(), backend.args(file, withAudio), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] });
|
||||||
|
proc = p;
|
||||||
|
p.on('error', () => {});
|
||||||
|
p.stdout.on('error', () => {});
|
||||||
|
p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); }));
|
||||||
|
if (p.stdio[3]) {
|
||||||
|
p.stdio[3].on('error', () => {});
|
||||||
|
p.stdio[3].on('data', (pcm) => { if (!ended && proc === p) onPcm(pcm); });
|
||||||
|
}
|
||||||
|
p.on('close', () => {
|
||||||
|
if (ended || proc !== p) return;
|
||||||
|
if (withAudio && !frames && backend.retryWithoutAudio) return run(false);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
run(true);
|
||||||
|
return { stop() { if (!ended) halt(); } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let streamFactory = systemStream;
|
||||||
|
|
||||||
|
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 = [];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
|
|
@ -223,7 +293,7 @@ const videoChunks = (seq, jpeg) => {
|
||||||
const plain = Buffer.alloc(Math.ceil((VIDEO_HDR + data.length) / VIDEO_PAD) * VIDEO_PAD);
|
const plain = Buffer.alloc(Math.ceil((VIDEO_HDR + data.length) / VIDEO_PAD) * VIDEO_PAD);
|
||||||
plain.writeUInt16BE(seq & 0xffff, 0);
|
plain.writeUInt16BE(seq & 0xffff, 0);
|
||||||
plain[2] = i;
|
plain[2] = i;
|
||||||
plain[3] = n;
|
plain[3] = n | (track ? VIDEO_FILM : 0);
|
||||||
plain.writeUInt16BE(data.length, 4);
|
plain.writeUInt16BE(data.length, 4);
|
||||||
data.copy(plain, VIDEO_HDR);
|
data.copy(plain, VIDEO_HDR);
|
||||||
nodeCrypto.randomFillSync(plain, VIDEO_HDR + data.length);
|
nodeCrypto.randomFillSync(plain, VIDEO_HDR + data.length);
|
||||||
|
|
@ -231,12 +301,13 @@ const videoChunks = (seq, jpeg) => {
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
const videoOff = () => { const plain = Buffer.alloc(VIDEO_PAD); nodeCrypto.randomFillSync(plain, VIDEO_HDR); return plain; };
|
const videoOff = (track = 0) => { const plain = Buffer.alloc(VIDEO_PAD); plain[3] = track ? VIDEO_FILM : 0; nodeCrypto.randomFillSync(plain, VIDEO_HDR); return plain; };
|
||||||
|
const videoTrack = (plain) => (Buffer.isBuffer(plain) && plain.length >= VIDEO_HDR && (plain[3] & VIDEO_FILM) ? 1 : 0);
|
||||||
const videoAssembler = () => {
|
const videoAssembler = () => {
|
||||||
let cur = null;
|
let cur = null;
|
||||||
return (plain) => {
|
return (plain) => {
|
||||||
if (!Buffer.isBuffer(plain) || plain.length < VIDEO_HDR) return null;
|
if (!Buffer.isBuffer(plain) || plain.length < VIDEO_HDR) return null;
|
||||||
const seq = plain.readUInt16BE(0), idx = plain[2], n = plain[3], len = plain.readUInt16BE(4);
|
const seq = plain.readUInt16BE(0), idx = plain[2], n = plain[3] & ~VIDEO_FILM, len = plain.readUInt16BE(4);
|
||||||
if (!n) { cur = null; return 'off'; }
|
if (!n) { cur = null; return 'off'; }
|
||||||
if (idx >= n || VIDEO_HDR + len > plain.length || n * VIDEO_CHUNK > VIDEO_FRAME_MAX) return null;
|
if (idx >= n || VIDEO_HDR + len > plain.length || n * VIDEO_CHUNK > VIDEO_FRAME_MAX) return null;
|
||||||
if (!cur || cur.seq !== seq) cur = { seq, n, parts: new Array(n), got: 0 };
|
if (!cur || cur.seq !== seq) cur = { seq, n, parts: new Array(n), got: 0 };
|
||||||
|
|
@ -418,7 +489,8 @@ module.exports = {
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
manifest: {
|
manifest: {
|
||||||
ring: 'async', answer: 'async', hangup: 'async', audio: 'duplex', relay: 'async', relayAudio: 'duplex', video: 'duplex', relayVideo: 'duplex',
|
ring: 'async', answer: 'async', hangup: 'async', audio: 'duplex', relay: 'async', relayAudio: 'duplex', video: 'duplex', relayVideo: 'duplex',
|
||||||
call: 'async', accept: 'async', reject: 'async', end: 'async', mute: 'async', silence: 'async', dismiss: 'async', camera: 'async', videoFrames: 'source',
|
call: 'async', accept: 'async', reject: 'async', end: 'async', mute: 'async', silence: 'async', dismiss: 'async', camera: 'async', stream: 'async', videoFrames: 'source',
|
||||||
|
viewerJoin: 'async', viewerLeave: 'async', viewerRooms: 'sync', publicFeed: 'source',
|
||||||
recordStop: 'async', recordCancel: 'async',
|
recordStop: 'async', recordCancel: 'async',
|
||||||
roomHub: 'duplex', roomInfo: 'async', roomAdmits: 'async', roomJoin: 'async', roomLeave: 'async', roomMute: 'async', roomHand: 'async', roomCount: 'async',
|
roomHub: 'duplex', roomInfo: 'async', roomAdmits: 'async', roomJoin: 'async', roomLeave: 'async', roomMute: 'async', roomHand: 'async', roomCount: 'async',
|
||||||
roomRecStart: 'async', roomRecStop: 'async', roomNotify: 'async', roomClearEvents: 'async',
|
roomRecStart: 'async', roomRecStop: 'async', roomNotify: 'async', roomClearEvents: 'async',
|
||||||
|
|
@ -453,6 +525,7 @@ module.exports = {
|
||||||
const roomMax = Number(policy().roomMax) > 0 ? Math.min(ROOM_MAX, Number(policy().roomMax)) : ROOM_MAX;
|
const roomMax = Number(policy().roomMax) > 0 ? Math.min(ROOM_MAX, Number(policy().roomMax)) : ROOM_MAX;
|
||||||
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 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'] : [])];
|
||||||
|
|
@ -568,11 +641,14 @@ module.exports = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOBILE_STATE = path.join(__dirname, '..', '..', '.phone_state.json');
|
const MOBILE_STATE = path.join(__dirname, '..', '..', '.phone_state.json');
|
||||||
const writeMobileState = (s) => {
|
let mobileCall = null;
|
||||||
|
let mobileRoom = null;
|
||||||
|
const writeMobileState = () => {
|
||||||
if (process.env.OASIS_MOBILE !== '1') return;
|
if (process.env.OASIS_MOBILE !== '1') return;
|
||||||
try { fs.writeFileSync(MOBILE_STATE, JSON.stringify(s || null)); } catch (_) {}
|
const s = mobileCall || mobileRoom ? { ...(mobileCall || {}), room: mobileRoom ? { phase: mobileRoom.phase, cam: !!mobileRoom.cam } : null } : null;
|
||||||
|
try { fs.writeFileSync(MOBILE_STATE, JSON.stringify(s)); } catch (_) {}
|
||||||
};
|
};
|
||||||
writeMobileState(null);
|
writeMobileState();
|
||||||
const emit = (ev) => { for (const p of subscribers) p.push(ev); };
|
const emit = (ev) => { for (const p of subscribers) p.push(ev); };
|
||||||
const snapshot = () => call ? {
|
const snapshot = () => call ? {
|
||||||
id: call.id, peer: call.peer, dir: call.dir, phase: call.phase,
|
id: call.id, peer: call.peer, dir: call.dir, phase: call.phase,
|
||||||
|
|
@ -582,7 +658,7 @@ module.exports = {
|
||||||
video: !!call.video, cam: !!call.cam, peerCam: !!call.peerCam, videoAvailable: videoOn(),
|
video: !!call.video, cam: !!call.cam, peerCam: !!call.peerCam, videoAvailable: videoOn(),
|
||||||
...(call.group ? { group: true, peers: call.legs.map(l => ({ id: l.peer, phase: l.phase, reached: !!l.reached, silenced: silenced.has(l.peer) })) } : {})
|
...(call.group ? { group: true, peers: call.legs.map(l => ({ id: l.peer, phase: l.phase, reached: !!l.reached, silenced: silenced.has(l.peer) })) } : {})
|
||||||
} : null;
|
} : null;
|
||||||
const changed = () => { const s = snapshot(); writeMobileState(s); emit({ type: 'state', state: s }); };
|
const changed = () => { const s = snapshot(); mobileCall = s; writeMobileState(); emit({ type: 'state', state: s }); };
|
||||||
const stopTone = (c) => { if (c.tone) { c.tone.stop(); c.tone = null; } };
|
const stopTone = (c) => { if (c.tone) { c.tone.stop(); c.tone = null; } };
|
||||||
const stopRecorder = (c) => { clearTimeout(c.recDelay); clearTimeout(c.recWarn); if (c.recorder) { c.recorder.stop(); c.recorder = null; } clearTimeout(c.recTimer); };
|
const stopRecorder = (c) => { clearTimeout(c.recDelay); clearTimeout(c.recWarn); if (c.recorder) { c.recorder.stop(); c.recorder = null; } clearTimeout(c.recTimer); };
|
||||||
const startRecording = (c) => {
|
const startRecording = (c) => {
|
||||||
|
|
@ -946,7 +1022,8 @@ module.exports = {
|
||||||
if (slot !== from) deliver(m, frame, droppable);
|
if (slot !== from) deliver(m, frame, droppable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const hubAttach = (rid, id, out, scope = null) => {
|
const hubHumans = (h) => [...h.members.values()].filter(m => !m.viewer).length;
|
||||||
|
const hubAttach = (rid, id, out, scope = null, viewer = false) => {
|
||||||
let r = hubRooms.get(rid);
|
let r = hubRooms.get(rid);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
if (hubRooms.size >= HUB_ROOMS_MAX) return null;
|
if (hubRooms.size >= HUB_ROOMS_MAX) return null;
|
||||||
|
|
@ -960,8 +1037,8 @@ module.exports = {
|
||||||
if (r.members.size >= roomMax) return null;
|
if (r.members.size >= roomMax) return null;
|
||||||
let slot = 0;
|
let slot = 0;
|
||||||
while (r.members.has(slot)) slot++;
|
while (r.members.has(slot)) slot++;
|
||||||
const peers = [...r.members].map(([s, m]) => ({ slot: s, id: m.id }));
|
const peers = [...r.members].map(([s, m]) => (m.viewer ? { slot: s, id: m.id, viewer: true } : { slot: s, id: m.id }));
|
||||||
const m = { id, out, sent: 0, acked: 0 };
|
const m = { id, out, sent: 0, acked: 0, viewer: !!viewer };
|
||||||
m.leave = () => {
|
m.leave = () => {
|
||||||
if (r.members.get(slot) !== m) return;
|
if (r.members.get(slot) !== m) return;
|
||||||
r.members.delete(slot);
|
r.members.delete(slot);
|
||||||
|
|
@ -1002,7 +1079,7 @@ module.exports = {
|
||||||
};
|
};
|
||||||
r.members.set(slot, m);
|
r.members.set(slot, m);
|
||||||
deliver(m, ctrlFrame({ t: 'hello', slot, peers, max: roomMax, video: policy().video !== false }), false);
|
deliver(m, ctrlFrame({ t: 'hello', slot, peers, max: roomMax, video: policy().video !== false }), false);
|
||||||
hubBroadcast(r, slot, ctrlFrame({ t: 'join', slot, id }), false);
|
hubBroadcast(r, slot, ctrlFrame(viewer ? { t: 'join', slot, id, viewer: true } : { t: 'join', slot, id }), false);
|
||||||
return m;
|
return m;
|
||||||
};
|
};
|
||||||
const hubDuplex = (from, opts) => {
|
const hubDuplex = (from, opts) => {
|
||||||
|
|
@ -1022,7 +1099,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
const admit = () => {
|
const admit = () => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
member = hubAttach(rid, from, out, roomId ? `${opts.owner}|${roomId}` : null);
|
member = hubAttach(rid, from, out, roomId ? `${opts.owner}|${roomId}` : null, opts.viewer === true);
|
||||||
if (!member) refuse('full');
|
if (!member) refuse('full');
|
||||||
};
|
};
|
||||||
if (opts.owner === server.id || from === server.id) admit();
|
if (opts.owner === server.id || from === server.id) admit();
|
||||||
|
|
@ -1030,24 +1107,29 @@ module.exports = {
|
||||||
return { source: out, sink };
|
return { source: out, sink };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewers = new Map();
|
||||||
|
const active = (r) => !!r && (r === room || viewers.get(r.rid) === r);
|
||||||
|
const touched = (r) => { if (r === room) roomChanged(); };
|
||||||
|
const humansIn = (r) => [...r.peers.values()].filter(p => !p.viewer).length;
|
||||||
const roomSnapshot = () => {
|
const roomSnapshot = () => {
|
||||||
const r = room;
|
const r = room;
|
||||||
if (!r) return null;
|
if (!r) return null;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return {
|
return {
|
||||||
rid: r.rid, ref: r.ref, title: r.title, phase: r.phase, joinedAt: r.joinedAt, muted: !!r.muted, hand: !!r.hand, handSince: r.hand ? r.handAt : 0,
|
rid: r.rid, ref: r.ref, title: r.title, phase: r.phase, joinedAt: r.joinedAt, muted: !!r.muted, hand: !!r.hand, handSince: r.hand ? r.handAt : 0,
|
||||||
secure: !!r.kid, count: r.peers.size + 1, max: r.max, 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,
|
||||||
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 }))
|
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 }))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const roomChanged = () => emit({ type: 'room', room: roomSnapshot() });
|
const roomChanged = () => { const r = roomSnapshot(); mobileRoom = r; writeMobileState(); emit({ type: 'room', room: r }); };
|
||||||
const pushEvent = (r, t, id) => {
|
const pushEvent = (r, t, id) => {
|
||||||
r.events.push({ t, id, ts: Date.now() });
|
r.events.push({ t, id, ts: Date.now() });
|
||||||
if (r.events.length > ROOM_EVENTS_MAX) r.events.splice(0, r.events.length - ROOM_EVENTS_MAX);
|
if (r.events.length > ROOM_EVENTS_MAX) r.events.splice(0, r.events.length - ROOM_EVENTS_MAX);
|
||||||
roomChanged();
|
touched(r);
|
||||||
};
|
};
|
||||||
const startRoomRec = (r) => {
|
const startRoomRec = (r) => {
|
||||||
if (!r || r.rec || r.phase !== 'live') return false;
|
if (!r || r.rec || r.phase !== 'live') return false;
|
||||||
|
|
@ -1102,8 +1184,8 @@ module.exports = {
|
||||||
const sealed = seal({ rid: r.rid, kid: r.kid, key: b64(key), from: server.id, ts, sig: signParts(keys, ['roomkey', r.rid, r.kid, p.id, ts]) }, p.id);
|
const sealed = seal({ rid: r.rid, kid: r.kid, key: b64(key), from: server.id, ts, sig: signParts(keys, ['roomkey', r.rid, r.kid, p.id, ts]) }, p.id);
|
||||||
if (sealed) r.out.push(Buffer.concat([Buffer.from([K_KEY, slot]), Buffer.from(sealed)]));
|
if (sealed) r.out.push(Buffer.concat([Buffer.from([K_KEY, slot]), Buffer.from(sealed)]));
|
||||||
};
|
};
|
||||||
const peerEntry = (id) => ({ id, queue: [], openers: new Map(), heardAt: 0, mutedAt: 0, dec: null, opus: false, greeted: false });
|
const peerEntry = (id, viewer = false) => ({ id, viewer: !!viewer, queue: [], openers: new Map(), heardAt: 0, mutedAt: 0, dec: null, opus: false, greeted: false });
|
||||||
const dropPeer = (r, slot) => { const p = r.peers.get(slot); if (p) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); endFrames(p.id); gridNote(p.id, null); } r.peers.delete(slot); };
|
const dropPeer = (r, slot) => { const p = r.peers.get(slot); if (p) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(r.tag + p.id); endFrames(r.tag + filmKey(p.id)); gridNote(r, p.id, null); gridNote(r, filmKey(p.id), null); } r.peers.delete(slot); };
|
||||||
const sendCodecs = (r) => { if (r.out && r.sealer && codecsOffered().length) sendRoomData(r, { t: 'codecs', codecs: codecsOffered() }); };
|
const sendCodecs = (r) => { if (r.out && r.sealer && codecsOffered().length) sendRoomData(r, { t: 'codecs', codecs: codecsOffered() }); };
|
||||||
const onRoomCtrl = (r, msg) => {
|
const onRoomCtrl = (r, msg) => {
|
||||||
if (msg.t === 'hello' && r.slot === null) {
|
if (msg.t === 'hello' && r.slot === null) {
|
||||||
|
|
@ -1112,7 +1194,7 @@ module.exports = {
|
||||||
r.hubVideo = msg.video === true;
|
r.hubVideo = msg.video === true;
|
||||||
for (const p of Array.isArray(msg.peers) ? msg.peers : []) {
|
for (const p of Array.isArray(msg.peers) ? msg.peers : []) {
|
||||||
if (!p || !isFeedId(p.id)) continue;
|
if (!p || !isFeedId(p.id)) continue;
|
||||||
r.peers.set(Number(p.slot), peerEntry(p.id));
|
r.peers.set(Number(p.slot), peerEntry(p.id, p.viewer === true));
|
||||||
r.order.push(Number(p.slot));
|
r.order.push(Number(p.slot));
|
||||||
}
|
}
|
||||||
r.order.push(r.slot);
|
r.order.push(r.slot);
|
||||||
|
|
@ -1123,10 +1205,10 @@ module.exports = {
|
||||||
} else if (msg.t === 'join' && isFeedId(msg.id)) {
|
} else if (msg.t === 'join' && isFeedId(msg.id)) {
|
||||||
const slot = Number(msg.slot);
|
const slot = Number(msg.slot);
|
||||||
dropPeer(r, slot);
|
dropPeer(r, slot);
|
||||||
r.peers.set(slot, peerEntry(msg.id));
|
r.peers.set(slot, peerEntry(msg.id, msg.viewer === true));
|
||||||
r.order = r.order.filter(s => s !== slot).concat(slot);
|
r.order = r.order.filter(s => s !== slot).concat(slot);
|
||||||
if (!r.static && keeperSlot(r) === r.slot) sendRoomKey(r, slot);
|
if (!r.static && keeperSlot(r) === r.slot) sendRoomKey(r, slot);
|
||||||
if (r.notify) playChime(audio, 'join');
|
if (r.notify && msg.viewer !== true) playChime(audio, 'join');
|
||||||
pushEvent(r, 'join', msg.id);
|
pushEvent(r, 'join', msg.id);
|
||||||
} else if (msg.t === 'leave') {
|
} else if (msg.t === 'leave') {
|
||||||
const slot = Number(msg.slot);
|
const slot = Number(msg.slot);
|
||||||
|
|
@ -1146,7 +1228,7 @@ module.exports = {
|
||||||
} else if (msg.t === 'full' || msg.t === 'refused') {
|
} else if (msg.t === 'full' || msg.t === 'refused') {
|
||||||
if (r.ready) return r.ready(new Error(msg.t));
|
if (r.ready) return r.ready(new Error(msg.t));
|
||||||
}
|
}
|
||||||
roomChanged();
|
touched(r);
|
||||||
};
|
};
|
||||||
const onRoomKey = (r, slot, payload) => {
|
const onRoomKey = (r, slot, payload) => {
|
||||||
const p = r.peers.get(slot);
|
const p = r.peers.get(slot);
|
||||||
|
|
@ -1157,7 +1239,7 @@ module.exports = {
|
||||||
const key = Buffer.from(String(msg.key || ''), 'base64');
|
const key = Buffer.from(String(msg.key || ''), 'base64');
|
||||||
if (key.length !== 32) return;
|
if (key.length !== 32) return;
|
||||||
useKey(r, msg.kid, key);
|
useKey(r, msg.kid, key);
|
||||||
roomChanged();
|
touched(r);
|
||||||
};
|
};
|
||||||
const openRoomFrame = (r, p, payload) => {
|
const openRoomFrame = (r, p, payload) => {
|
||||||
if (!p || payload.length < 44) return null;
|
if (!p || payload.length < 44) return null;
|
||||||
|
|
@ -1187,11 +1269,11 @@ module.exports = {
|
||||||
const noteMute = (r, p, on, quiet = false) => {
|
const noteMute = (r, p, on, quiet = false) => {
|
||||||
if (!!p.muted === on) return;
|
if (!!p.muted === on) return;
|
||||||
p.muted = on;
|
p.muted = on;
|
||||||
if (quiet) return roomChanged();
|
if (quiet) return touched(r);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const announce = now - (p.muteToggleAt || 0) >= REC_TOGGLE_MIN_MS;
|
const announce = now - (p.muteToggleAt || 0) >= REC_TOGGLE_MIN_MS;
|
||||||
p.muteToggleAt = now;
|
p.muteToggleAt = now;
|
||||||
if (!announce) return roomChanged();
|
if (!announce) return touched(r);
|
||||||
if (r.notify) playChime(audio, on ? 'mute' : 'unmute');
|
if (r.notify) playChime(audio, on ? 'mute' : 'unmute');
|
||||||
pushEvent(r, on ? 'mute' : 'unmute', p.id);
|
pushEvent(r, on ? 'mute' : 'unmute', p.id);
|
||||||
};
|
};
|
||||||
|
|
@ -1200,10 +1282,10 @@ module.exports = {
|
||||||
p.hand = on;
|
p.hand = on;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
p.handSince = on ? (quiet && Number.isFinite(at) && at > 0 ? Math.min(at, now) : now) : 0;
|
p.handSince = on ? (quiet && Number.isFinite(at) && at > 0 ? Math.min(at, now) : now) : 0;
|
||||||
if (!on || quiet) return roomChanged();
|
if (!on || quiet) return touched(r);
|
||||||
const announce = now - (p.handToggleAt || 0) >= REC_TOGGLE_MIN_MS;
|
const announce = now - (p.handToggleAt || 0) >= REC_TOGGLE_MIN_MS;
|
||||||
p.handToggleAt = now;
|
p.handToggleAt = now;
|
||||||
if (!announce) return roomChanged();
|
if (!announce) return touched(r);
|
||||||
if (r.notify) playChime(audio, 'hand');
|
if (r.notify) playChime(audio, 'hand');
|
||||||
pushEvent(r, 'hand', p.id);
|
pushEvent(r, 'hand', p.id);
|
||||||
};
|
};
|
||||||
|
|
@ -1226,7 +1308,7 @@ module.exports = {
|
||||||
p.recToggleAt = now;
|
p.recToggleAt = now;
|
||||||
if (msg.on) r.recPeers.add(p.id); else r.recPeers.delete(p.id);
|
if (msg.on) r.recPeers.add(p.id); else r.recPeers.delete(p.id);
|
||||||
if (announce) { playChime(audio, msg.on ? 'recStart' : 'recStop'); pushEvent(r, msg.on ? 'recStart' : 'recStop', p.id); }
|
if (announce) { playChime(audio, msg.on ? 'recStart' : 'recStop'); pushEvent(r, msg.on ? 'recStart' : 'recStop', p.id); }
|
||||||
else roomChanged();
|
else touched(r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg && msg.t === 'hand') {
|
if (msg && msg.t === 'hand') {
|
||||||
|
|
@ -1240,64 +1322,146 @@ module.exports = {
|
||||||
};
|
};
|
||||||
const gridIds = (r) => {
|
const gridIds = (r) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const ids = r.cam ? [server.id] : [];
|
const ids = [];
|
||||||
for (const slot of r.order) { const p = r.peers.get(slot); if (p && now - (p.frameAt || 0) < VIDEO_HOLD_MS) ids.push(p.id); }
|
if (r.cam) ids.push(server.id);
|
||||||
|
if (r.stream) ids.push(filmKey(server.id));
|
||||||
|
for (const slot of r.order) {
|
||||||
|
const p = r.peers.get(slot);
|
||||||
|
if (!p) continue;
|
||||||
|
if (now - (p.frameAt || 0) < VIDEO_HOLD_MS) ids.push(p.id);
|
||||||
|
if (now - (p.filmAt || 0) < VIDEO_HOLD_MS) ids.push(filmKey(p.id));
|
||||||
|
}
|
||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
const gridFrames = new Map();
|
const newGrid = () => ({ frames: new Map(), busy: false, dirty: false, timer: null });
|
||||||
let gridBusy = false;
|
|
||||||
let gridDirty = false;
|
|
||||||
let gridTimer = null;
|
|
||||||
const composeGrid = async (r) => {
|
const composeGrid = async (r) => {
|
||||||
const ids = gridIds(r).filter(id => gridFrames.has(id));
|
const g = r.grid;
|
||||||
if (!ids.length) return;
|
const ids = gridIds(r).filter(id => g.frames.has(id));
|
||||||
|
if (!ids.length) {
|
||||||
|
if (r.tag && !g.empty) { g.empty = true; const b = await blankFrame(); if (b && active(r)) publishFrame(r.tag + 'grid', b); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g.empty = false;
|
||||||
|
if (ids.length === 1 && ids[0] !== server.id) { if (active(r)) publishFrame(r.tag + 'grid', g.frames.get(ids[0])); return; }
|
||||||
|
const size = ids.length <= GRID_LARGE_MAX ? GRID_TILE_LARGE : VIDEO_SIZE;
|
||||||
const cols = Math.ceil(Math.sqrt(ids.length));
|
const cols = Math.ceil(Math.sqrt(ids.length));
|
||||||
const rows = Math.ceil(ids.length / cols);
|
const rows = Math.ceil(ids.length / cols);
|
||||||
const tiles = [];
|
const tiles = [];
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
try {
|
try {
|
||||||
let tile = sharpLib(gridFrames.get(ids[i])).resize(VIDEO_SIZE.w, VIDEO_SIZE.h, { fit: 'cover' });
|
let tile = sharpLib(g.frames.get(ids[i])).resize(size.w, size.h, { fit: 'contain', background: '#000000' });
|
||||||
if (ids[i] === server.id) tile = tile.flop();
|
if (ids[i] === server.id) tile = tile.flop();
|
||||||
tiles.push({ input: await tile.toBuffer(), left: (i % cols) * VIDEO_SIZE.w, top: Math.floor(i / cols) * VIDEO_SIZE.h });
|
tiles.push({ input: await tile.toBuffer(), left: (i % cols) * size.w, top: Math.floor(i / cols) * size.h });
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!tiles.length) return;
|
if (!tiles.length) return;
|
||||||
const jpeg = await sharpLib({ create: { width: cols * VIDEO_SIZE.w, height: rows * VIDEO_SIZE.h, channels: 3, background: '#000000' } }).composite(tiles).jpeg({ quality: 70 }).toBuffer();
|
const jpeg = await sharpLib({ create: { width: cols * size.w, height: rows * size.h, channels: 3, background: '#000000' } }).composite(tiles).jpeg({ quality: 80 }).toBuffer();
|
||||||
if (room === r) publishFrame('grid', jpeg);
|
if (active(r)) publishFrame(r.tag + 'grid', jpeg);
|
||||||
};
|
};
|
||||||
const gridTick = () => {
|
const gridTick = (r) => {
|
||||||
const r = room;
|
const g = r && r.grid;
|
||||||
if (!r || !sharpLib || gridBusy || !gridDirty || !frameWatchers.has('grid')) return;
|
if (!g || !active(r) || !sharpLib || g.busy || !g.dirty || !frameWatchers.has(r.tag + 'grid')) return;
|
||||||
gridDirty = false;
|
g.dirty = false;
|
||||||
gridBusy = true;
|
g.busy = true;
|
||||||
composeGrid(r).catch(() => {}).then(() => { gridBusy = false; });
|
composeGrid(r).catch(() => {}).then(() => { g.busy = false; });
|
||||||
};
|
};
|
||||||
const gridNote = (id, jpeg) => { if (jpeg) gridFrames.set(id, jpeg); else gridFrames.delete(id); gridDirty = true; };
|
const gridNote = (r, id, jpeg) => { if (!r || !r.grid) return; if (jpeg) r.grid.frames.set(id, jpeg); else r.grid.frames.delete(id); r.grid.dirty = true; };
|
||||||
const stopGrid = () => { clearInterval(gridTimer); gridTimer = null; gridFrames.clear(); gridDirty = false; endFrames('grid'); };
|
const startGrid = (r) => { if (!sharpLib || !r.grid || r.grid.timer) return; r.grid.timer = setInterval(() => gridTick(r), Math.round(1000 / VIDEO_FPS)); if (r.grid.timer.unref) r.grid.timer.unref(); };
|
||||||
|
const stopGrid = (r) => { if (!r || !r.grid) return; clearInterval(r.grid.timer); r.grid.timer = null; r.grid.frames.clear(); r.grid.dirty = false; endFrames(r.tag + 'grid'); };
|
||||||
const onRoomVideo = (r, slot, payload) => {
|
const onRoomVideo = (r, slot, payload) => {
|
||||||
const p = r.peers.get(slot);
|
const p = r.peers.get(slot);
|
||||||
if (!videoAllowed()) return;
|
if (!videoAllowed()) return;
|
||||||
const plain = openRoomFrame(r, p, payload);
|
const plain = openRoomFrame(r, p, payload);
|
||||||
if (!plain) return;
|
if (!plain) return;
|
||||||
if (!p.vasm) p.vasm = videoAssembler();
|
const film = videoTrack(plain) === 1;
|
||||||
const got = p.vasm(plain);
|
const key = film ? filmKey(p.id) : p.id;
|
||||||
if (got === 'off') { clearTimeout(p.vtimer); if (p.frameAt) { p.frameAt = 0; endFrames(p.id); gridNote(p.id, null); roomChanged(); } return; }
|
const at = film ? 'filmAt' : 'frameAt';
|
||||||
|
const timer = film ? 'ftimer' : 'vtimer';
|
||||||
|
const asm = film ? 'fasm' : 'vasm';
|
||||||
|
if (!p[asm]) p[asm] = videoAssembler();
|
||||||
|
const got = p[asm](plain);
|
||||||
|
if (got === 'off') { clearTimeout(p[timer]); if (p[at]) { p[at] = 0; endFrames(r.tag + key); gridNote(r, key, null); touched(r); } return; }
|
||||||
if (!got) return;
|
if (!got) return;
|
||||||
const fresh = !(Date.now() - (p.frameAt || 0) < VIDEO_HOLD_MS);
|
const fresh = !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS);
|
||||||
p.frameAt = Date.now();
|
p[at] = Date.now();
|
||||||
clearTimeout(p.vtimer);
|
clearTimeout(p[timer]);
|
||||||
p.vtimer = setTimeout(() => { if (room === r) { endFrames(p.id); gridNote(p.id, null); roomChanged(); } }, VIDEO_HOLD_MS);
|
p[timer] = setTimeout(() => { if (active(r)) { endFrames(r.tag + key); gridNote(r, key, null); touched(r); } }, VIDEO_HOLD_MS);
|
||||||
if (p.vtimer.unref) p.vtimer.unref();
|
if (p[timer].unref) p[timer].unref();
|
||||||
const hold = setTimeout(() => { if (room !== r || !(Date.now() - (p.frameAt || 0) < VIDEO_HOLD_MS)) return; publishFrame(p.id, got); gridNote(p.id, got); }, p.queue.length * FRAME_MS);
|
const hold = setTimeout(() => { if (!active(r) || !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS)) return; publishFrame(r.tag + key, got); gridNote(r, key, got); }, p.queue.length * FRAME_MS);
|
||||||
if (hold.unref) hold.unref();
|
if (hold.unref) hold.unref();
|
||||||
if (fresh) roomChanged();
|
if (fresh) touched(r);
|
||||||
};
|
};
|
||||||
const sendRoomVideo = (r, plain) => {
|
const sendRoomVideo = (r, plain) => {
|
||||||
if (!r.sealer || !r.hubVideo) return;
|
if (!r.sealer || !r.hubVideo) return;
|
||||||
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 streamFile = (file) => {
|
||||||
|
const raw = String(file || '');
|
||||||
|
if (!path.isAbsolute(raw)) return null;
|
||||||
|
try { const full = fs.realpathSync(raw); return fs.statSync(full).isFile() ? full : null; } catch (_) { return null; }
|
||||||
|
};
|
||||||
|
const filmKey = (id) => `film:${id}`;
|
||||||
|
const videoBudget = (r, chunks) => {
|
||||||
|
const bytes = chunks.reduce((n, c) => n + c.length + ROOM_VIDEO_CHUNK_EXTRA, 0);
|
||||||
|
const now = Date.now();
|
||||||
|
const sent = r.vsent || (r.vsent = []);
|
||||||
|
while (sent.length && now - sent[0][0] >= 1000) sent.shift();
|
||||||
|
if (sent.reduce((n, e) => n + e[1], 0) + bytes > ROOM_VIDEO_SEND_MAX) return false;
|
||||||
|
sent.push([now, bytes]);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const sendRoomJpeg = (r, seq, jpeg, track = 0) => {
|
||||||
|
const chunks = videoChunks(seq, jpeg, track);
|
||||||
|
if (!videoBudget(r, chunks)) return;
|
||||||
|
for (const plain of chunks) sendRoomVideo(r, plain);
|
||||||
|
};
|
||||||
|
const stopRoomStream = (r, notify = true) => {
|
||||||
|
const s = r && r.stream;
|
||||||
|
if (!s) return false;
|
||||||
|
r.stream = null;
|
||||||
|
try { if (s.handle) s.handle.stop(); } catch (_) {}
|
||||||
|
endFrames('film:me');
|
||||||
|
gridNote(r, filmKey(server.id), null);
|
||||||
|
if (room === r) sendRoomVideo(r, videoOff(1));
|
||||||
|
if (notify) roomChanged();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const startRoomStream = (r, file, name) => {
|
||||||
|
if (!r || r.phase !== 'live' || !r.hubVideo || !fileStreamOn()) return false;
|
||||||
|
stopRoomStream(r, false);
|
||||||
|
const s = { name: String(name || '').slice(0, 120), pcm: Buffer.alloc(0), seq: 0, handle: null };
|
||||||
|
r.stream = s;
|
||||||
|
try {
|
||||||
|
s.handle = streamer.open(file, {
|
||||||
|
onJpeg: (jpeg) => {
|
||||||
|
if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return;
|
||||||
|
s.seq = (s.seq + 1) & 0xffff;
|
||||||
|
publishFrame('film:me', jpeg);
|
||||||
|
gridNote(r, filmKey(server.id), jpeg);
|
||||||
|
sendRoomJpeg(r, s.seq, jpeg, 1);
|
||||||
|
},
|
||||||
|
onPcm: (pcm) => {
|
||||||
|
if (r.stream !== s || !pcm || !pcm.length) return;
|
||||||
|
s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm);
|
||||||
|
const drop = (s.pcm.length - RATE * 2) & ~1;
|
||||||
|
if (drop > 0) s.pcm = s.pcm.subarray(drop);
|
||||||
|
},
|
||||||
|
onEnd: () => { if (r.stream === s) stopRoomStream(r); }
|
||||||
|
});
|
||||||
|
} catch (_) { s.handle = null; }
|
||||||
|
if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; }
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const streamFrame = (r) => {
|
||||||
|
const s = r.stream;
|
||||||
|
if (!s || s.pcm.length < FRAME_BYTES) return null;
|
||||||
|
const f = s.pcm.subarray(0, FRAME_BYTES);
|
||||||
|
s.pcm = s.pcm.subarray(FRAME_BYTES);
|
||||||
|
return f;
|
||||||
|
};
|
||||||
const onRoomFrame = (r, frame) => {
|
const onRoomFrame = (r, frame) => {
|
||||||
if (room !== r) return;
|
if (!active(r)) return;
|
||||||
const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame || []);
|
const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame || []);
|
||||||
if (!buf.length) return;
|
if (!buf.length) return;
|
||||||
r.received = (r.received || 0) + 1;
|
r.received = (r.received || 0) + 1;
|
||||||
|
|
@ -1327,6 +1491,8 @@ module.exports = {
|
||||||
pending = pending.subarray(FRAME_BYTES);
|
pending = pending.subarray(FRAME_BYTES);
|
||||||
const acc = new Int32Array(samples);
|
const acc = new Int32Array(samples);
|
||||||
for (const p of r.peers.values()) { const f = p.queue.shift(); if (f && !silenced.has(p.id)) mixInto(acc, f); }
|
for (const p of r.peers.values()) { const f = p.queue.shift(); if (f && !silenced.has(p.id)) mixInto(acc, f); }
|
||||||
|
const film = streamFrame(r);
|
||||||
|
if (film) mixInto(acc, film);
|
||||||
player.write(clampPcm(acc));
|
player.write(clampPcm(acc));
|
||||||
if (r.rec) {
|
if (r.rec) {
|
||||||
const both = Int32Array.from(acc);
|
const both = Int32Array.from(acc);
|
||||||
|
|
@ -1339,11 +1505,14 @@ module.exports = {
|
||||||
const voiced = gate(mic);
|
const voiced = gate(mic);
|
||||||
if (r.muted && r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true });
|
if (r.muted && r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true });
|
||||||
if (r.hand && r.sealer && Date.now() - (r.handBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'hand', on: true, at: r.handAt, beacon: true }); r.handBeaconAt = Date.now(); }
|
if (r.hand && r.sealer && Date.now() - (r.handBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'hand', on: true, at: r.handAt, beacon: true }); r.handBeaconAt = Date.now(); }
|
||||||
if (r.muted || !r.sealer || !voiced) continue;
|
const talk = !r.muted && voiced;
|
||||||
|
if (!r.sealer || (!talk && !film)) continue;
|
||||||
r.sentAt = Date.now();
|
r.sentAt = Date.now();
|
||||||
|
let send = mic;
|
||||||
|
if (film) { const mix = new Int32Array(samples); mixInto(mix, film); if (talk) mixInto(mix, mic); send = clampPcm(mix); }
|
||||||
const opus = codecsOffered().includes('opus') && r.peers.size > 0 && [...r.peers.values()].every(p => p.opus);
|
const opus = codecsOffered().includes('opus') && r.peers.size > 0 && [...r.peers.values()].every(p => p.opus);
|
||||||
if (opus && !r.enc) r.enc = makeEncoder('opus');
|
if (opus && !r.enc) r.enc = makeEncoder('opus');
|
||||||
r.out.push(Buffer.concat([Buffer.from([K_AUDIO]), Buffer.from(r.kid, 'hex'), r.sealer(opus ? r.enc.encode(mic) : ulawEncode(mic))]));
|
r.out.push(Buffer.concat([Buffer.from([K_AUDIO]), Buffer.from(r.kid, 'hex'), r.sealer(opus ? r.enc.encode(send) : ulawEncode(send))]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
r.media = { stop() { capture.stop(); player.stop(); } };
|
r.media = { stop() { capture.stop(); player.stop(); } };
|
||||||
|
|
@ -1359,9 +1528,10 @@ module.exports = {
|
||||||
stopRoomRec(r);
|
stopRoomRec(r);
|
||||||
room = null;
|
room = null;
|
||||||
clearTimeout(r.timer);
|
clearTimeout(r.timer);
|
||||||
|
stopRoomStream(r, false);
|
||||||
if (r.cam) { r.cam = false; stopCam(); }
|
if (r.cam) { r.cam = false; stopCam(); }
|
||||||
for (const p of r.peers.values()) { clearTimeout(p.vtimer); endFrames(p.id); }
|
for (const p of r.peers.values()) { clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(p.id); endFrames(filmKey(p.id)); }
|
||||||
stopGrid();
|
stopGrid(r);
|
||||||
if (r.media) r.media.stop();
|
if (r.media) r.media.stop();
|
||||||
if (r.enc) { r.enc.free(); r.enc = null; }
|
if (r.enc) { r.enc.free(); r.enc = null; }
|
||||||
for (const p of r.peers.values()) if (p.dec) p.dec.free();
|
for (const p of r.peers.values()) if (p.dec) p.dec.free();
|
||||||
|
|
@ -1370,13 +1540,57 @@ module.exports = {
|
||||||
if (outcome) emit({ type: 'roomEnded', room: { rid: r.rid, ref: r.ref, title: r.title, joinedAt: r.joinedAt, endedAt: Date.now(), outcome } });
|
if (outcome) emit({ type: 'roomEnded', room: { rid: r.rid, ref: r.ref, title: r.title, joinedAt: r.joinedAt, endedAt: Date.now(), outcome } });
|
||||||
roomChanged();
|
roomChanged();
|
||||||
};
|
};
|
||||||
|
const openHub = (opts, req, attach) => {
|
||||||
|
if (opts.hub === server.id) return attach(hubDuplex(server.id, req));
|
||||||
|
const viaRpc = (rpc) => attach(rpc && rpc.phone && typeof rpc.phone.roomHub === 'function' ? rpc.phone.roomHub(req, () => {}) : null);
|
||||||
|
const live = liveRpc(opts.hub);
|
||||||
|
if (live) return viaRpc(live);
|
||||||
|
if (typeof opts.address === 'string' && opts.address) {
|
||||||
|
try {
|
||||||
|
return server.conn.connect(opts.address, (err, rpc) => {
|
||||||
|
const got = !err && rpc && rpc.phone ? rpc : liveRpc(opts.hub);
|
||||||
|
return got ? viaRpc(got) : reach(opts.hub, viaRpc);
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
reach(opts.hub, viaRpc);
|
||||||
|
};
|
||||||
|
let blank = null;
|
||||||
|
const blankFrame = async () => {
|
||||||
|
if (blank || !sharpLib) return blank;
|
||||||
|
try { blank = await sharpLib({ create: { width: GRID_TILE_LARGE.w, height: GRID_TILE_LARGE.h, channels: 3, background: '#000000' } }).jpeg().toBuffer(); } catch (_) { blank = null; }
|
||||||
|
return blank;
|
||||||
|
};
|
||||||
|
const soundKey = (r) => r.tag + 'sound';
|
||||||
|
const dropViewer = (r) => {
|
||||||
|
if (!r || viewers.get(r.rid) !== r) return;
|
||||||
|
viewers.delete(r.rid);
|
||||||
|
clearTimeout(r.timer);
|
||||||
|
clearInterval(r.mixTimer);
|
||||||
|
stopGrid(r);
|
||||||
|
endFrames(soundKey(r));
|
||||||
|
for (const p of r.peers.values()) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); }
|
||||||
|
try { r.out.push(ctrlFrame({ t: 'bye' })); } catch (_) {}
|
||||||
|
try { r.out.end(); } catch (_) {}
|
||||||
|
};
|
||||||
|
const startViewerMedia = (r) => {
|
||||||
|
const samples = FRAME_BYTES / 2;
|
||||||
|
r.mixTimer = setInterval(() => {
|
||||||
|
if (viewers.get(r.rid) !== r) return;
|
||||||
|
const acc = new Int32Array(samples);
|
||||||
|
for (const p of r.peers.values()) { const f = p.queue.shift(); if (f) mixInto(acc, f); }
|
||||||
|
if (frameWatchers.has(soundKey(r))) publishFrame(soundKey(r), clampPcm(acc));
|
||||||
|
if (r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true });
|
||||||
|
}, FRAME_MS);
|
||||||
|
if (r.mixTimer.unref) r.mixTimer.unref();
|
||||||
|
};
|
||||||
const staticKey = (rid, secret) => {
|
const staticKey = (rid, secret) => {
|
||||||
const key = Buffer.from(sodium.crypto_hash_sha256(Buffer.concat([Buffer.from('oasis-room|'), Buffer.from(rid, 'hex'), Buffer.from(String(secret))])));
|
const key = Buffer.from(sodium.crypto_hash_sha256(Buffer.concat([Buffer.from('oasis-room|'), Buffer.from(rid, 'hex'), Buffer.from(String(secret))])));
|
||||||
return { kid: Buffer.from(sodium.crypto_hash_sha256(key)).subarray(0, 4).toString('hex'), key };
|
return { kid: Buffer.from(sodium.crypto_hash_sha256(key)).subarray(0, 4).toString('hex'), key };
|
||||||
};
|
};
|
||||||
|
|
||||||
if (server.close && typeof server.close.hook === 'function') {
|
if (server.close && typeof server.close.hook === 'function') {
|
||||||
server.close.hook(function (fn, args) { finish('ended'); leaveRoom(room, 'ended'); stopCam(); return fn.apply(this, args); });
|
server.close.hook(function (fn, args) { finish('ended'); leaveRoom(room, 'ended'); stopCam(); for (const v of [...viewers.values()]) dropViewer(v); return fn.apply(this, args); });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1566,12 +1780,12 @@ module.exports = {
|
||||||
if (r && r.phase === 'live') {
|
if (r && r.phase === 'live') {
|
||||||
if (on === !!r.cam) return cb(null, roomSnapshot());
|
if (on === !!r.cam) return cb(null, roomSnapshot());
|
||||||
if (on) {
|
if (on) {
|
||||||
if (!r.hubVideo || !startCam((jpeg, seq) => { if (room !== r) return; gridNote(server.id, jpeg); for (const plain of videoChunks(seq, jpeg)) sendRoomVideo(r, plain); })) return cb(new Error('unavailable'));
|
if (!r.hubVideo || !startCam((jpeg, seq) => { if (room !== r) return; gridNote(r, server.id, jpeg); sendRoomJpeg(r, seq, jpeg, 0); })) return cb(new Error('unavailable'));
|
||||||
r.cam = true;
|
r.cam = true;
|
||||||
} else {
|
} else {
|
||||||
r.cam = false;
|
r.cam = false;
|
||||||
stopCam();
|
stopCam();
|
||||||
gridNote(server.id, null);
|
gridNote(r, server.id, null);
|
||||||
sendRoomVideo(r, videoOff());
|
sendRoomVideo(r, videoOff());
|
||||||
}
|
}
|
||||||
roomChanged();
|
roomChanged();
|
||||||
|
|
@ -1579,13 +1793,78 @@ module.exports = {
|
||||||
}
|
}
|
||||||
cb(new Error('no-call'));
|
cb(new Error('no-call'));
|
||||||
},
|
},
|
||||||
|
stream(opts, cb) {
|
||||||
|
const o = opts && typeof opts === 'object' ? opts : {};
|
||||||
|
const r = room;
|
||||||
|
if (!r || r.phase !== 'live') return cb(new Error('no-room'));
|
||||||
|
if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); }
|
||||||
|
const file = streamFile(o.file);
|
||||||
|
if (!file) return cb(new Error('invalid'));
|
||||||
|
if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable'));
|
||||||
|
roomChanged();
|
||||||
|
cb(null, roomSnapshot());
|
||||||
|
},
|
||||||
|
viewerJoin(opts, cb) {
|
||||||
|
const o = opts && typeof opts === 'object' ? opts : {};
|
||||||
|
const rid = String(o.rid || '');
|
||||||
|
if (!RID.test(rid) || !isFeedId(o.owner) || !isFeedId(o.hub) || typeof o.token !== 'string') return cb(new Error('invalid'));
|
||||||
|
if (viewers.has(rid)) return cb(null, true);
|
||||||
|
if (room && room.rid === rid) return cb(new Error('busy'));
|
||||||
|
const r = {
|
||||||
|
rid, ref: String(o.ref || ''), title: '', hub: o.hub, phase: 'joining', joinedAt: Date.now(), muted: true, slot: null, max: ROOM_MAX,
|
||||||
|
peers: new Map(), order: [], keys: new Map(), kid: null, sealer: null, static: false, sentAt: 0,
|
||||||
|
events: [], notify: false, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: `pub:${rid}:`, grid: newGrid()
|
||||||
|
};
|
||||||
|
viewers.set(rid, r);
|
||||||
|
r.out = Pushable(() => {});
|
||||||
|
r.ready = (err) => {
|
||||||
|
r.ready = null;
|
||||||
|
clearTimeout(r.timer);
|
||||||
|
if (err) { dropViewer(r); return cb(err); }
|
||||||
|
startViewerMedia(r);
|
||||||
|
startGrid(r);
|
||||||
|
cb(null, true);
|
||||||
|
};
|
||||||
|
r.timer = setTimeout(() => { if (r.ready) r.ready(new Error('unreachable')); }, ROOM_JOIN_MS);
|
||||||
|
const attach = (duplex) => {
|
||||||
|
if (viewers.get(rid) !== r || !r.ready) return closeDuplex(duplex);
|
||||||
|
if (!duplex) return r.ready(new Error('unreachable'));
|
||||||
|
pull(r.out, duplex.sink);
|
||||||
|
pull(duplex.source, pull.drain((frame) => onRoomFrame(r, frame), () => {
|
||||||
|
if (viewers.get(rid) !== r) return;
|
||||||
|
if (r.ready) r.ready(new Error('unreachable'));
|
||||||
|
else dropViewer(r);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
openHub(o, { rid, owner: o.owner, token: o.token, viewer: true, ...(RID.test(String(o.roomId || '')) ? { roomId: String(o.roomId) } : {}) }, attach);
|
||||||
|
},
|
||||||
|
viewerLeave(opts, cb) {
|
||||||
|
dropViewer(viewers.get(String((opts && opts.rid) || '')));
|
||||||
|
cb(null, true);
|
||||||
|
},
|
||||||
|
viewerRooms() { return [...viewers.values()].filter(r => r.phase === 'live').map(r => r.rid); },
|
||||||
|
publicFeed(opts) {
|
||||||
|
const rid = String((opts && opts.rid) || '');
|
||||||
|
const kind = opts && opts.kind === 'sound' ? 'sound' : 'grid';
|
||||||
|
const r = viewers.get(rid);
|
||||||
|
const target = r ? r.tag + kind : '';
|
||||||
|
const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } });
|
||||||
|
if (!r || r.phase !== 'live' || (kind === 'grid' && !sharpLib)) { p.end(); return p; }
|
||||||
|
if (!frameWatchers.has(target)) frameWatchers.set(target, new Set());
|
||||||
|
frameWatchers.get(target).add(p);
|
||||||
|
if (kind === 'grid') { r.grid.dirty = true; r.grid.empty = false; }
|
||||||
|
return p;
|
||||||
|
},
|
||||||
videoFrames(opts) {
|
videoFrames(opts) {
|
||||||
const target = String((opts && opts.peer) || '');
|
const target = String((opts && opts.peer) || '');
|
||||||
const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } });
|
const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const filmOf = target.startsWith('film:') && target !== 'film:me' ? target.slice(5) : null;
|
||||||
const live = target === 'me'
|
const live = target === 'me'
|
||||||
? !!cam
|
? !!cam
|
||||||
|
: target === 'film:me' ? !!(room && room.stream)
|
||||||
: target === 'grid' ? !!(room && sharpLib && gridIds(room).length)
|
: target === 'grid' ? !!(room && sharpLib && gridIds(room).length)
|
||||||
|
: filmOf ? !!room && [...room.peers.values()].some(x => x.id === filmOf && now - (x.filmAt || 0) < VIDEO_HOLD_MS)
|
||||||
: call ? (call.peer === target && !!call.peerCam)
|
: call ? (call.peer === target && !!call.peerCam)
|
||||||
: room ? [...room.peers.values()].some(x => x.id === target && now - (x.frameAt || 0) < VIDEO_HOLD_MS)
|
: room ? [...room.peers.values()].some(x => x.id === target && now - (x.frameAt || 0) < VIDEO_HOLD_MS)
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -1694,7 +1973,7 @@ module.exports = {
|
||||||
roomHub(opts) { return hubDuplex(this && this.id, opts); },
|
roomHub(opts) { return hubDuplex(this && this.id, opts); },
|
||||||
roomInfo(opts, cb) {
|
roomInfo(opts, cb) {
|
||||||
const r = hubRooms.get(String((opts && opts.rid) || ''));
|
const r = hubRooms.get(String((opts && opts.rid) || ''));
|
||||||
cb(null, { count: r ? r.members.size : 0, max: roomMax, video: policy().video !== false });
|
cb(null, { count: r ? hubHumans(r) : 0, max: roomMax, video: policy().video !== false });
|
||||||
},
|
},
|
||||||
roomAdmits(opts, cb) {
|
roomAdmits(opts, cb) {
|
||||||
const from = this && this.id;
|
const from = this && this.id;
|
||||||
|
|
@ -1705,13 +1984,14 @@ module.exports = {
|
||||||
if (!audio) return cb(new Error('unavailable'));
|
if (!audio) return cb(new Error('unavailable'));
|
||||||
if (call || room) return cb(new Error('busy'));
|
if (call || room) return cb(new Error('busy'));
|
||||||
const rid = String((opts && opts.rid) || '');
|
const rid = String((opts && opts.rid) || '');
|
||||||
|
if (viewers.has(rid)) dropViewer(viewers.get(rid));
|
||||||
if (!RID.test(rid) || !isFeedId(opts.owner) || !isFeedId(opts.hub) || typeof opts.token !== 'string') return cb(new Error('invalid'));
|
if (!RID.test(rid) || !isFeedId(opts.owner) || !isFeedId(opts.hub) || typeof opts.token !== 'string') return cb(new Error('invalid'));
|
||||||
const secrets = (Array.isArray(opts.secrets) ? opts.secrets : []).filter(x => typeof x === 'string' && x);
|
const secrets = (Array.isArray(opts.secrets) ? opts.secrets : []).filter(x => typeof x === 'string' && x);
|
||||||
const r = {
|
const r = {
|
||||||
rid, ref: String(opts.ref || ''), title: String(opts.title || '').slice(0, 200), hub: opts.hub,
|
rid, ref: String(opts.ref || ''), title: String(opts.title || '').slice(0, 200), hub: opts.hub,
|
||||||
phase: 'joining', joinedAt: Date.now(), muted: false, slot: null, max: ROOM_MAX, peers: new Map(), order: [],
|
phase: 'joining', joinedAt: Date.now(), muted: false, slot: null, max: ROOM_MAX, peers: new Map(), order: [],
|
||||||
keys: new Map(), kid: null, sealer: null, static: secrets.length > 0, sentAt: 0,
|
keys: new Map(), kid: null, sealer: null, static: secrets.length > 0, sentAt: 0,
|
||||||
events: [], notify: true, rec: null, recPeers: new Set(), hand: false, handAt: 0
|
events: [], notify: true, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: '', grid: newGrid()
|
||||||
};
|
};
|
||||||
if (r.static) {
|
if (r.static) {
|
||||||
for (const secret of secrets.slice().reverse()) { const k = staticKey(rid, secret); r.keys.set(k.kid, k.key); }
|
for (const secret of secrets.slice().reverse()) { const k = staticKey(rid, secret); r.keys.set(k.kid, k.key); }
|
||||||
|
|
@ -1726,7 +2006,7 @@ module.exports = {
|
||||||
clearTimeout(r.timer);
|
clearTimeout(r.timer);
|
||||||
if (err) { leaveRoom(r, null); return cb(err); }
|
if (err) { leaveRoom(r, null); return cb(err); }
|
||||||
startRoomMedia(r);
|
startRoomMedia(r);
|
||||||
if (sharpLib) { gridTimer = setInterval(gridTick, Math.round(1000 / VIDEO_FPS)); if (gridTimer.unref) gridTimer.unref(); }
|
startGrid(r);
|
||||||
roomChanged();
|
roomChanged();
|
||||||
cb(null, roomSnapshot());
|
cb(null, roomSnapshot());
|
||||||
};
|
};
|
||||||
|
|
@ -1741,20 +2021,7 @@ module.exports = {
|
||||||
else leaveRoom(r, 'lost');
|
else leaveRoom(r, 'lost');
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
const req = { rid, owner: opts.owner, token: opts.token, ...(RID.test(String(opts.roomId || '')) ? { roomId: String(opts.roomId) } : {}) };
|
openHub(opts, { rid, owner: opts.owner, token: opts.token, ...(RID.test(String(opts.roomId || '')) ? { roomId: String(opts.roomId) } : {}) }, attach);
|
||||||
if (opts.hub === server.id) return attach(hubDuplex(server.id, req));
|
|
||||||
const viaRpc = (rpc) => attach(rpc && rpc.phone && typeof rpc.phone.roomHub === 'function' ? rpc.phone.roomHub(req, () => {}) : null);
|
|
||||||
const live = liveRpc(opts.hub);
|
|
||||||
if (live) return viaRpc(live);
|
|
||||||
if (typeof opts.address === 'string' && opts.address) {
|
|
||||||
try {
|
|
||||||
return server.conn.connect(opts.address, (err, rpc) => {
|
|
||||||
const got = !err && rpc && rpc.phone ? rpc : liveRpc(opts.hub);
|
|
||||||
return got ? viaRpc(got) : reach(opts.hub, viaRpc);
|
|
||||||
});
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
reach(opts.hub, viaRpc);
|
|
||||||
},
|
},
|
||||||
roomLeave(cb) {
|
roomLeave(cb) {
|
||||||
leaveRoom(room, 'left');
|
leaveRoom(room, 'left');
|
||||||
|
|
@ -1787,8 +2054,8 @@ module.exports = {
|
||||||
roomCount(opts, cb) {
|
roomCount(opts, cb) {
|
||||||
const rid = String((opts && opts.rid) || '');
|
const rid = String((opts && opts.rid) || '');
|
||||||
if (!RID.test(rid) || !isFeedId(opts.hub)) return cb(null, null);
|
if (!RID.test(rid) || !isFeedId(opts.hub)) return cb(null, null);
|
||||||
if (room && room.rid === rid && room.phase === 'live') return cb(null, { count: room.peers.size + 1, max: room.max });
|
if (room && room.rid === rid && room.phase === 'live') return cb(null, { count: humansIn(room) + 1, max: room.max });
|
||||||
if (opts.hub === server.id) { const h = hubRooms.get(rid); return cb(null, { count: h ? h.members.size : 0, max: roomMax }); }
|
if (opts.hub === server.id) { const h = hubRooms.get(rid); return cb(null, { count: h ? hubHumans(h) : 0, max: roomMax }); }
|
||||||
const rpc = liveRpc(opts.hub);
|
const rpc = liveRpc(opts.hub);
|
||||||
if (!rpc || typeof rpc.phone.roomInfo !== 'function') return cb(null, null);
|
if (!rpc || typeof rpc.phone.roomInfo !== 'function') return cb(null, null);
|
||||||
let done = false;
|
let done = false;
|
||||||
|
|
@ -1840,6 +2107,7 @@ 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.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;
|
||||||
|
|
@ -1853,3 +2121,4 @@ module.exports.RING_PER_WINDOW = RING_PER_WINDOW;
|
||||||
module.exports.chimeMs = chimeMs;
|
module.exports.chimeMs = chimeMs;
|
||||||
module.exports.recordingPrefix = recordingPrefix;
|
module.exports.recordingPrefix = recordingPrefix;
|
||||||
module.exports.VOICEMAIL_MAX_MS = VOICEMAIL_MAX_MS;
|
module.exports.VOICEMAIL_MAX_MS = VOICEMAIL_MAX_MS;
|
||||||
|
module.exports.SOUND_RATE = RATE;
|
||||||
|
|
|
||||||
53
src/server/phone_stream_android.js
Normal file
53
src/server/phone_stream_android.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const net = require('net');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const MARKER = path.join(__dirname, '..', '..', '.audio_bridge');
|
||||||
|
const FRAME_MAX = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
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 = () => ({
|
||||||
|
open(file, { onJpeg, onPcm, onEnd }) {
|
||||||
|
let ended = false;
|
||||||
|
const finish = () => {
|
||||||
|
if (ended) return;
|
||||||
|
ended = true;
|
||||||
|
try { onEnd(); } catch (_) {}
|
||||||
|
};
|
||||||
|
const port = bridgePort();
|
||||||
|
if (!port) {
|
||||||
|
setImmediate(finish);
|
||||||
|
return { stop() { ended = true; } };
|
||||||
|
}
|
||||||
|
const sock = net.connect(port, '127.0.0.1');
|
||||||
|
let buf = Buffer.alloc(0);
|
||||||
|
sock.setNoDelay(true);
|
||||||
|
sock.on('error', finish);
|
||||||
|
sock.on('close', finish);
|
||||||
|
sock.on('connect', () => sock.write(Buffer.concat([Buffer.from('S'), Buffer.from(`${file}\n`, 'utf8')])));
|
||||||
|
sock.on('data', (chunk) => {
|
||||||
|
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
|
||||||
|
while (buf.length >= 5) {
|
||||||
|
const len = buf.readUInt32BE(1);
|
||||||
|
if (len > FRAME_MAX) { sock.destroy(); return; }
|
||||||
|
if (buf.length < 5 + len) return;
|
||||||
|
const type = buf[0];
|
||||||
|
const payload = Buffer.from(buf.subarray(5, 5 + len));
|
||||||
|
buf = buf.subarray(5 + len);
|
||||||
|
if (ended) continue;
|
||||||
|
if (type === 0x4a) onJpeg(payload);
|
||||||
|
else if (type === 0x41) onPcm(payload);
|
||||||
|
else if (type === 0x45) { finish(); sock.destroy(); return; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { stop() { ended = true; try { sock.destroy(); } catch (_) {} } };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -47,9 +47,9 @@ config.blobs.max = 75 * megabyte;
|
||||||
config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) };
|
config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) };
|
||||||
|
|
||||||
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 || lanOn ? '0.0.0.0' : '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) || []) {
|
||||||
if (entry && typeof entry === 'object' && !entry.host) entry.host = bindHost;
|
if (entry && typeof entry === 'object' && !entry.host && bindHost) entry.host = bindHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.account = account;
|
config.account = account;
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,9 @@ const blobUsageOnDisk = (ssbPath) => {
|
||||||
|
|
||||||
let printed = false;
|
let printed = false;
|
||||||
let checkedForUpdate = false;
|
let checkedForUpdate = false;
|
||||||
let pendingClearnetModules = null;
|
|
||||||
let clearnetReady = false;
|
|
||||||
let pendingWarmup = null;
|
let pendingWarmup = null;
|
||||||
let warmupReady = false;
|
let warmupReady = false;
|
||||||
|
|
||||||
function setClearnetModules(modules) {
|
|
||||||
pendingClearnetModules = Array.isArray(modules) ? modules : [];
|
|
||||||
clearnetReady = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWarmupTime(str) {
|
function setWarmupTime(str) {
|
||||||
pendingWarmup = str;
|
pendingWarmup = str;
|
||||||
warmupReady = true;
|
warmupReady = true;
|
||||||
|
|
@ -81,19 +74,6 @@ async function checkForUpdate() {
|
||||||
await updater.getRemoteVersion();
|
await updater.getRemoteVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForClearnet(timeoutMs = 25000) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (clearnetReady) return resolve(pendingClearnetModules || []);
|
|
||||||
const started = Date.now();
|
|
||||||
const tick = () => {
|
|
||||||
if (clearnetReady) return resolve(pendingClearnetModules || []);
|
|
||||||
if (Date.now() - started >= timeoutMs) return resolve(null);
|
|
||||||
setTimeout(tick, 250);
|
|
||||||
};
|
|
||||||
tick();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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', offline = false, isPublic = false) {
|
||||||
if (printed) return;
|
if (printed) return;
|
||||||
printed = true;
|
printed = true;
|
||||||
|
|
@ -151,8 +131,6 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt
|
||||||
console.log(`- Media cache limit: ${cacheMb > 0 ? formatMB(cacheMb * 1024 * 1024) : 'Unlimited'}`);
|
console.log(`- Media cache limit: ${cacheMb > 0 ? formatMB(cacheMb * 1024 * 1024) : 'Unlimited'}`);
|
||||||
console.log(`- Current usage: ${formatMB(cacheUsage.bytes, 1)} · ${cacheUsage.files} files`);
|
console.log(`- Current usage: ${formatMB(cacheUsage.bytes, 1)} · ${cacheUsage.files} files`);
|
||||||
console.log(`- LAN Broadcasting (UDP): ${localDiscovery ? 'enabled' : 'disabled'}`);
|
console.log(`- LAN Broadcasting (UDP): ${localDiscovery ? 'enabled' : 'disabled'}`);
|
||||||
const clearnetModules = await waitForClearnet();
|
|
||||||
const clearnetStatus = (clearnetModules && clearnetModules.length > 0) ? clearnetModules.join(', ') : 'disabled';
|
|
||||||
const multiverse = (() => {
|
const multiverse = (() => {
|
||||||
if ((oasisCfg.modules || {}).fediverseMod === 'off') return [];
|
if ((oasisCfg.modules || {}).fediverseMod === 'off') return [];
|
||||||
try {
|
try {
|
||||||
|
|
@ -164,12 +142,10 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
} catch (_) { return []; }
|
} catch (_) { return []; }
|
||||||
})();
|
})();
|
||||||
console.log(`- Internet Broadcasting:`);
|
console.log(`- Multiverse: ${multiverse.length ? multiverse.join(', ') : 'disabled'}`);
|
||||||
console.log(` - Clearnet: ${clearnetStatus}`);
|
|
||||||
console.log(` - Multiverse: ${multiverse.length ? multiverse.join(', ') : 'disabled'}`);
|
|
||||||
if (hasHttp) {
|
if (hasHttp) {
|
||||||
const en = (() => { try { return require('../client/assets/translations/i18n').en || {}; } catch (_) { return {}; } })();
|
const en = (() => { try { return require('../client/assets/translations/i18n').en || {}; } catch (_) { return {}; } })();
|
||||||
const uxNames = { blocks: en.uxModeMenus, ainav: en.uxModeAINav, chats: en.uxModeChats, feed: en.uxModeFeed, phone: en.phoneTitle };
|
const uxNames = { blocks: en.uxModeMenus, ainav: en.uxModeAINav, chats: en.uxModeChats, feed: en.uxModeFeed, phone: en.phoneTitle, rooms: en.roomsTitle };
|
||||||
const wishNames = { whole: en.settingsWishWhole, mutuals: en.settingsWishMutuals, 'only-lan': en.settingsWishOnlyLan, local: en.settingsWishLocal };
|
const wishNames = { whole: en.settingsWishWhole, mutuals: en.settingsWishMutuals, 'only-lan': en.settingsWishOnlyLan, local: en.settingsWishLocal };
|
||||||
const workflow = (() => {
|
const workflow = (() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -202,7 +178,6 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
printMetadata,
|
printMetadata,
|
||||||
setClearnetModules,
|
|
||||||
setWarmupTime,
|
setWarmupTime,
|
||||||
colors
|
colors
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -837,7 +837,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
)
|
)
|
||||||
: '',
|
: '',
|
||||||
contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
|
contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
|
||||||
bodyNode
|
bodyNode,
|
||||||
|
Array.isArray(content?.tags) && content.tags.filter(Boolean).length
|
||||||
|
? div({ class: 'card-tags' }, content.tags.filter(Boolean).map(tag =>
|
||||||
|
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||||
|
: ''
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1883,6 +1887,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
||||||
),
|
),
|
||||||
renderContentActions(msgId, detailHref, {
|
renderContentActions(msgId, detailHref, {
|
||||||
author: action.author,
|
author: action.author,
|
||||||
|
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,
|
||||||
...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}),
|
...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const {
|
||||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||||
|
|
||||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = 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");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText")
|
const { renderStyledText } = require("../backend/renderStyledText")
|
||||||
|
|
@ -61,7 +61,7 @@ const renderAudioPlayer = (audioObj, opts = {}) =>
|
||||||
audioObj?.url
|
audioObj?.url
|
||||||
? div(
|
? div(
|
||||||
{ class: "audio-container" },
|
{ class: "audio-container" },
|
||||||
audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(audioObj.url)}`, preload: "metadata" }),
|
audioHyperaxe({ controls: true, src: blobUrlOf(audioObj.url), preload: "metadata" }),
|
||||||
opts.skipTranscode ? null : renderTranscodeButton(audioObj)
|
opts.skipTranscode ? null : renderTranscodeButton(audioObj)
|
||||||
)
|
)
|
||||||
: p(i18n.audioNoFile);
|
: p(i18n.audioNoFile);
|
||||||
|
|
@ -236,7 +236,6 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {})
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||||
|
|
@ -380,7 +379,6 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe");
|
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe");
|
||||||
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||||
const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged } = require("../views/main_views");
|
const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged, renderIcon } = require("../views/main_views");
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
|
|
||||||
const FILTER_LABELS = {
|
const FILTER_LABELS = {
|
||||||
|
|
@ -489,7 +489,7 @@ const renderUbiPubs = (pubs, hasValidWallet) => {
|
||||||
userLink(p.pubId),
|
userLink(p.pubId),
|
||||||
p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null,
|
p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null,
|
||||||
hasValidWallet && p.address
|
hasValidWallet && p.address
|
||||||
? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, "\u2744")))
|
? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, renderIcon("donate"))))
|
||||||
: null
|
: null
|
||||||
),
|
),
|
||||||
div({ class: "bank-summary ubi-pub-summary" },
|
div({ class: "bank-summary ubi-pub-summary" },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe");
|
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n, userLink, paged } = require("../views/main_views");
|
const { template, i18n, userLink, paged, renderIcon } = require("../views/main_views");
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
|
|
||||||
const FILTER_LABELS = {
|
const FILTER_LABELS = {
|
||||||
|
|
@ -175,7 +175,9 @@ const getViewDetailsAction = (type, block) => {
|
||||||
case 'event': return `/events/${encodeURIComponent(block.id)}`;
|
case 'event': return `/events/${encodeURIComponent(block.id)}`;
|
||||||
case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
|
case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
|
||||||
case 'about': return `/author/${encodeURIComponent(block.author)}`;
|
case 'about': return `/author/${encodeURIComponent(block.author)}`;
|
||||||
case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`;
|
case 'post': return block.content && typeof block.content.root === 'string'
|
||||||
|
? `/thread/${encodeURIComponent(block.content.root)}#${encodeURIComponent(block.id)}`
|
||||||
|
: `/blogs/${encodeURIComponent(block.id)}`;
|
||||||
case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
|
case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
|
||||||
case 'contact': return `/inhabitants`;
|
case 'contact': return `/inhabitants`;
|
||||||
case 'pub': return `/invites`;
|
case 'pub': return `/invites`;
|
||||||
|
|
@ -424,7 +426,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi
|
||||||
button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
|
button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
|
||||||
),
|
),
|
||||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open'))
|
||||||
: (block.isTombstoned || block.isReplaced) ?
|
: (block.isTombstoned || block.isReplaced) ?
|
||||||
div({ class: 'deleted-label' },
|
div({ class: 'deleted-label' },
|
||||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||||
|
|
@ -534,10 +536,10 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {})
|
||||||
.map(block=>
|
.map(block=>
|
||||||
div({ class:'block' },
|
div({ class:'block' },
|
||||||
div({ class:'block-buttons' },
|
div({ class:'block-buttons' },
|
||||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'),
|
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, renderIcon('block')),
|
||||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, '⊞'),
|
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, renderIcon('datagram')),
|
||||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open'))
|
||||||
: (block.isTombstoned || block.isReplaced) ?
|
: (block.isTombstoned || block.isReplaced) ?
|
||||||
div({ class: 'deleted-label' },
|
div({ class: 'deleted-label' },
|
||||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,16 @@ const FILTERS = [
|
||||||
|
|
||||||
const safeText = (v) => String(v == null ? "" : v).trim();
|
const safeText = (v) => String(v == null ? "" : v).trim();
|
||||||
|
|
||||||
|
const renderBlogTags = (tags) => {
|
||||||
|
const list = (Array.isArray(tags) ? tags : []).map(t => String(t || "").trim().replace(/^#+/, "")).filter(Boolean);
|
||||||
|
return list.length
|
||||||
|
? div({ class: "card-tags" }, list.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
const excerpt = (text, max = 420) => {
|
const excerpt = (text, max = 420) => {
|
||||||
const s = safeText(text);
|
const s = safeText(text);
|
||||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
return s.length > max ? s.slice(0, max).replace(/\s+$/, "") + "…" : s;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFilterBar = (filter, q, showSearch = true, total = null, censusList = null) => {
|
const renderFilterBar = (filter, q, showSearch = true, total = null, censusList = null) => {
|
||||||
|
|
@ -77,6 +84,7 @@ const renderBlogCard = (blog, filter, spreadInfo) => {
|
||||||
: null,
|
: null,
|
||||||
blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null,
|
blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null,
|
||||||
div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }),
|
div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }),
|
||||||
|
renderBlogTags(blog.tags),
|
||||||
p({ class: "card-footer" },
|
p({ class: "card-footer" },
|
||||||
span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`),
|
span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`),
|
||||||
userLink(blog.author)
|
userLink(blog.author)
|
||||||
|
|
@ -89,13 +97,15 @@ const renderCreateForm = (draft = null) => {
|
||||||
const subjectValue = draft ? safeText(draft.subject) : "";
|
const subjectValue = draft ? safeText(draft.subject) : "";
|
||||||
const textValue = draft ? String(draft.text || "") : "";
|
const textValue = draft ? String(draft.text || "") : "";
|
||||||
const allowComments = draft ? draft.allowComments !== false : true;
|
const allowComments = draft ? draft.allowComments !== false : true;
|
||||||
|
const tagsValue = draft ? (Array.isArray(draft.tags) ? draft.tags.join(", ") : safeText(draft.tags)) : "";
|
||||||
return section(
|
return section(
|
||||||
draft
|
draft
|
||||||
? section({ class: "post-preview blog-preview" },
|
? section({ class: "post-preview blog-preview" },
|
||||||
div({ class: "preview-content" },
|
div({ class: "preview-content" },
|
||||||
h2(i18n.messagePreview),
|
h2(i18n.messagePreview),
|
||||||
subjectValue ? h2({ class: "tribe-card-title" }, subjectValue) : null,
|
subjectValue ? h2({ class: "tribe-card-title" }, subjectValue) : null,
|
||||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) })
|
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) }),
|
||||||
|
renderBlogTags(draft.tags)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -105,17 +115,19 @@ const renderCreateForm = (draft = null) => {
|
||||||
br(),
|
br(),
|
||||||
input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder, value: subjectValue }),
|
input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder, value: subjectValue }),
|
||||||
br(),
|
br(),
|
||||||
br(),
|
|
||||||
label({ for: "text" }, i18n.blogMessage),
|
label({ for: "text" }, i18n.blogMessage),
|
||||||
br(),
|
br(),
|
||||||
textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: String(TEXT_CAP), placeholder: i18n.publishWarningPlaceholder }, textValue),
|
textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: String(TEXT_CAP), placeholder: i18n.publishWarningPlaceholder }, textValue),
|
||||||
br(),
|
br(),
|
||||||
br(),
|
|
||||||
label({ for: "blob" }, i18n.blogMedia),
|
label({ for: "blob" }, i18n.blogMedia),
|
||||||
br(),
|
br(),
|
||||||
input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }),
|
input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }),
|
||||||
br(),
|
br(),
|
||||||
br(),
|
br(),
|
||||||
|
label({ for: "tags" }, i18n.tagsLabel || "Tags"),
|
||||||
|
br(),
|
||||||
|
input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder || "", value: tagsValue }),
|
||||||
|
br(),
|
||||||
div({ class: "blog-allow-comments" },
|
div({ class: "blog-allow-comments" },
|
||||||
input({ type: "hidden", name: "allowComments", value: "0" }),
|
input({ type: "hidden", name: "allowComments", value: "0" }),
|
||||||
label(
|
label(
|
||||||
|
|
@ -206,7 +218,8 @@ exports.singleBlogView = async (blog, comments = [], params = {}) => {
|
||||||
|
|
||||||
const blogMain = div({ class: "tribe-main" },
|
const blogMain = div({ class: "tribe-main" },
|
||||||
div({ class: "job-section" },
|
div({ class: "job-section" },
|
||||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) })
|
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) }),
|
||||||
|
renderBlogTags(blog.tags)
|
||||||
),
|
),
|
||||||
renderEngagement(blog.id,
|
renderEngagement(blog.id,
|
||||||
renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants),
|
renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants),
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,6 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
||||||
|
|
@ -369,7 +368,6 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
||||||
section(
|
section(
|
||||||
div({ class: "filters" },
|
div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/campaigns", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/campaigns", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||||
...chipButtons(STATUSES, filter, census, statusLabel),
|
...chipButtons(STATUSES, filter, census, statusLabel),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const catLabel = (c) => i18n[catKey(c)] || c
|
||||||
|
|
||||||
const blobSrcOf = (value) => {
|
const blobSrcOf = (value) => {
|
||||||
const s = String(value || "").trim()
|
const s = String(value || "").trim()
|
||||||
|
if (s.startsWith("/blob/sealed/")) return s
|
||||||
if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}`
|
if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}`
|
||||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||||
return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null
|
return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null
|
||||||
|
|
@ -30,9 +31,10 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => {
|
||||||
if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||||
const s = String(value).trim()
|
const s = String(value).trim()
|
||||||
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||||
|
if (s.startsWith("/blob/sealed/")) return img({ loading: 'lazy', src: s, ...attrs })
|
||||||
if (s.startsWith("&")) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
if (s.startsWith("&")) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
||||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||||
if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
if (mImg) return img({ loading: 'lazy', src: mImg[1].startsWith("/blob/sealed/") ? mImg[1] : `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
||||||
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,47 @@ const renderClearnetUrlBlock = ({ baseUrl = '', path, i18nObj = {} }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CLEARNET_ZOOM_CSS = `
|
||||||
|
.cn-zoom{display:contents}
|
||||||
|
.cn-zoom img{cursor:zoom-in}
|
||||||
|
.cn-lightbox{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.9);align-items:center;justify-content:center}
|
||||||
|
.cn-lightbox:target{display:flex}
|
||||||
|
.cn-lightbox-link{display:flex;align-items:center;justify-content:center;width:100%;height:100%;cursor:zoom-out}
|
||||||
|
.cn-lightbox-img{display:block;max-width:92vw;max-height:88vh;object-fit:contain}
|
||||||
|
.cn-lightbox-full{background:#000}
|
||||||
|
.cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh;max-width:100vw;max-height:100vh}
|
||||||
|
.cn-lightbox-close,.cn-lightbox-fs{position:absolute;top:14px;z-index:1;width:42px;height:42px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;text-decoration:none;font-size:24px;line-height:1}
|
||||||
|
.cn-lightbox-close{right:16px}
|
||||||
|
.cn-lightbox-fs{right:66px}
|
||||||
|
.cn-lightbox-fs svg{display:block;width:20px;height:20px}
|
||||||
|
`;
|
||||||
|
const CN_FULLSCREEN_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 9 4 4 9 4"/><polyline points="15 4 20 4 20 9"/><polyline points="20 15 20 20 15 20"/><polyline points="9 20 4 20 4 15"/></svg>';
|
||||||
|
const cnZoom = (id, src, alt = '', { full = false, imgClass = '' } = {}) => {
|
||||||
|
const zid = `cnz-${String(id).replace(/[^A-Za-z0-9_-]/g, '')}`;
|
||||||
|
const a = escapeHtml(alt);
|
||||||
|
const u = escapeHtml(src);
|
||||||
|
const cls = imgClass ? ` class="${escapeHtml(imgClass)}"` : '';
|
||||||
|
const fs = full ? `<a class="cn-lightbox-fs" href="#${zid}-full" title="${escapeHtml(cnText('liveFullscreen', 'Full screen'))}">${CN_FULLSCREEN_ICON}</a>` : '';
|
||||||
|
return `<a class="cn-zoom" id="${zid}-src" href="#${zid}"><img${cls} src="${u}" alt="${a}"/></a>`
|
||||||
|
+ `<div class="cn-lightbox" id="${zid}"><a class="cn-lightbox-close" href="#${zid}-src">×</a>${fs}<a class="cn-lightbox-link" href="#${zid}-src"><img class="cn-lightbox-img" src="${u}" alt="${a}" loading="lazy"/></a></div>`
|
||||||
|
+ (full ? `<div class="cn-lightbox cn-lightbox-full" id="${zid}-full"><a class="cn-lightbox-close" href="#${zid}">×</a><a class="cn-lightbox-link" href="#${zid}"><img class="cn-lightbox-img" src="${u}" alt="${a}" loading="lazy"/></a></div>` : '');
|
||||||
|
};
|
||||||
|
const CN_ZOOM_SKIP = /\bcn-(lightbox-img|footer-logo)\b/;
|
||||||
|
const zoomClearnetImages = (html) => {
|
||||||
|
let depth = 0;
|
||||||
|
let n = 0;
|
||||||
|
return String(html || '').replace(/<a\b[^>]*>|<\/a\s*>|<img\b[^>]*>/gi, (tag) => {
|
||||||
|
if (/^<a\b/i.test(tag)) { depth++; return tag; }
|
||||||
|
if (/^<\/a/i.test(tag)) { depth = Math.max(0, depth - 1); return tag; }
|
||||||
|
if (depth > 0) return tag;
|
||||||
|
const cls = (tag.match(/\sclass="([^"]*)"/i) || [])[1] || '';
|
||||||
|
const src = (tag.match(/\ssrc="([^"]*)"/i) || [])[1] || '';
|
||||||
|
if (!src || CN_ZOOM_SKIP.test(cls)) return tag;
|
||||||
|
const alt = (tag.match(/\salt="([^"]*)"/i) || [])[1] || '';
|
||||||
|
const zid = `cnz-i${++n}`;
|
||||||
|
return `<a class="cn-zoom" id="${zid}-src" href="#${zid}">${tag}</a><span class="cn-lightbox" id="${zid}"><a class="cn-lightbox-close" href="#${zid}-src">×</a><a class="cn-lightbox-link" href="#${zid}-src"><img class="cn-lightbox-img" src="${src}" alt="${alt}" loading="lazy"/></a></span>`;
|
||||||
|
});
|
||||||
|
};
|
||||||
const CLEARNET_TEXT_CSS = `
|
const CLEARNET_TEXT_CSS = `
|
||||||
.cn-tags{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
|
.cn-tags{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
|
||||||
.cn-hub-priceline{margin:10px 0 0 0}
|
.cn-hub-priceline{margin:10px 0 0 0}
|
||||||
|
|
@ -266,8 +307,11 @@ a.tag-link:hover{text-decoration:underline}
|
||||||
.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0}
|
.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0}
|
||||||
.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word}
|
.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word}
|
||||||
.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word}
|
.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word}
|
||||||
.post-image{display:block;max-width:100%;height:auto;margin:12px 0;border:1px solid var(--border);border-radius:6px}
|
.post-image{display:block;width:auto;max-width:100%;max-height:480px;height:auto;margin:12px auto;border:1px solid var(--border);border-radius:6px}
|
||||||
.post-video,.post-audio{display:block;width:100%;max-width:100%;margin:12px 0;border-radius:6px;background:#000}
|
.post-video{display:block;width:100%;max-width:100%;max-height:480px;margin:12px auto;border-radius:6px;background:#000}
|
||||||
|
.post-audio{display:block;width:100%;max-width:100%;margin:12px 0}
|
||||||
|
.cn-media-frame img{max-height:480px;width:auto;margin:0 auto}
|
||||||
|
.cn-media-frame video{max-height:480px}
|
||||||
.post-pdf{display:inline-block;margin:8px 0;padding:8px 14px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);text-decoration:none}
|
.post-pdf{display:inline-block;margin:8px 0;padding:8px 14px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);text-decoration:none}
|
||||||
.post-pdf:hover{border-color:var(--fg)}
|
.post-pdf:hover{border-color:var(--fg)}
|
||||||
`;
|
`;
|
||||||
|
|
@ -346,7 +390,10 @@ footer.cn-footer .cn-footer-logo{width:56px;height:auto;display:block;margin:0 a
|
||||||
footer.cn-footer .cn-footer-line{margin:3px 0}
|
footer.cn-footer .cn-footer-line{margin:3px 0}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = '', hubFeedId = null }) => {
|
const renderClearnetHubSearch = ({ type = '', query = '' } = {}) =>
|
||||||
|
`<form class="cn-search" method="GET" action="/c">${type ? `<input type="hidden" name="type" value="${escapeHtml(type)}"/>` : ''}<input type="text" name="q" value="${escapeHtml(query || '')}" placeholder="${escapeHtml(cnText('cnSearchPlaceholder', 'Search…'))}" autocomplete="off"/></form>`;
|
||||||
|
|
||||||
|
const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null }) => {
|
||||||
const safeTitle = escapeHtml(title || 'Oasis');
|
const safeTitle = escapeHtml(title || 'Oasis');
|
||||||
const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis');
|
const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis');
|
||||||
const safeOgDesc = escapeHtml(ogDescription || '');
|
const safeOgDesc = escapeHtml(ogDescription || '');
|
||||||
|
|
@ -368,7 +415,7 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null
|
||||||
<meta name="description" content="${safeOgDesc}"/>
|
<meta name="description" content="${safeOgDesc}"/>
|
||||||
<meta name="robots" content="index, follow"/>
|
<meta name="robots" content="index, follow"/>
|
||||||
<link rel="icon" href="/c/assets/images/favicon.svg"/>
|
<link rel="icon" href="/c/assets/images/favicon.svg"/>
|
||||||
<style>${baseCss}${CLEARNET_SEARCH_CSS}${CLEARNET_TEXT_CSS}${extraCss}
|
<style>${baseCss}${CLEARNET_SEARCH_CSS}${CLEARNET_TEXT_CSS}${CLEARNET_ZOOM_CSS}${extraCss}
|
||||||
.cn-brand-link{display:block;text-decoration:none}
|
.cn-brand-link{display:block;text-decoration:none}
|
||||||
|
|
||||||
.cn-brand-link:hover .cn-brand{color:var(--accent)}
|
.cn-brand-link:hover .cn-brand{color:var(--accent)}
|
||||||
|
|
@ -386,9 +433,9 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null
|
||||||
<body>
|
<body>
|
||||||
<header class="cn-header">
|
<header class="cn-header">
|
||||||
${brandBlock}
|
${brandBlock}
|
||||||
<div class="cn-header-extra">${renderLangSelector(lang)}${propagateLang(headerExtra || '', langOverride)}</div>
|
<div class="cn-header-extra">${renderLangSelector(lang)}${propagateLang(headerExtra == null ? renderClearnetHubSearch() : headerExtra, langOverride)}</div>
|
||||||
</header>
|
</header>
|
||||||
${propagateLang(stripInternalAnchors(body), langOverride)}
|
${propagateLang(stripInternalAnchors(zoomClearnetImages(body)), langOverride)}
|
||||||
<footer class="cn-footer">
|
<footer class="cn-footer">
|
||||||
<a href="https://wiki.solarnethub.com" target="_blank" rel="noopener"><img class="cn-footer-logo" src="/c/assets/images/snh-oasis.jpg" alt="Oasis"/></a>
|
<a href="https://wiki.solarnethub.com" target="_blank" rel="noopener"><img class="cn-footer-logo" src="/c/assets/images/snh-oasis.jpg" alt="Oasis"/></a>
|
||||||
<div class="cn-footer-line">${escapeHtml(cnText('cnSyncedPeers', 'Synced-peers'))}: [ <strong>${Number(sharedState.getSyncedPeerCount ? sharedState.getSyncedPeerCount() : 0) || 0}</strong> ]</div>
|
<div class="cn-footer-line">${escapeHtml(cnText('cnSyncedPeers', 'Synced-peers'))}: [ <strong>${Number(sharedState.getSyncedPeerCount ? sharedState.getSyncedPeerCount() : 0) || 0}</strong> ]</div>
|
||||||
|
|
@ -550,6 +597,7 @@ const renderClearnetPodcastView = ({ channel }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
cnZoom,
|
||||||
kindLabel,
|
kindLabel,
|
||||||
renderTagChips,
|
renderTagChips,
|
||||||
renderClearnetPodcastView,
|
renderClearnetPodcastView,
|
||||||
|
|
@ -575,6 +623,7 @@ module.exports = {
|
||||||
renderDoubleEncryptionChip,
|
renderDoubleEncryptionChip,
|
||||||
renderClearnetUrlBlock,
|
renderClearnetUrlBlock,
|
||||||
renderClearnetSearchForm,
|
renderClearnetSearchForm,
|
||||||
|
renderClearnetHubSearch,
|
||||||
renderClearnetPage,
|
renderClearnetPage,
|
||||||
renderClearnetNotFound,
|
renderClearnetNotFound,
|
||||||
CLEARNET_PAGER_CSS,
|
CLEARNET_PAGER_CSS,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe");
|
const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe");
|
||||||
const { TEXT_CAP } = require('../backend/long_text');
|
const { TEXT_CAP } = require('../backend/long_text');
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { i18n, userLink } = require("./main_views");
|
const { i18n, userLink, renderHideIcon, renderIcon } = require("./main_views");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText");
|
const { renderStyledText } = require("../backend/renderStyledText");
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).
|
||||||
return t && String(t).trim();
|
return t && String(t).trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCommentCard = (c, extra = null) => {
|
const renderCommentCard = (c, extra = null, returnTo = null) => {
|
||||||
const author = (c && c.value && c.value.author) || "";
|
const author = (c && c.value && c.value.author) || "";
|
||||||
const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp);
|
const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp);
|
||||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : "";
|
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : "";
|
||||||
|
|
@ -28,7 +28,13 @@ const renderCommentCard = (c, extra = null) => {
|
||||||
return div({ class: own ? "votations-comment-card own-comment" : "votations-comment-card", id: c && c.key ? c.key : undefined },
|
return div({ class: own ? "votations-comment-card own-comment" : "votations-comment-card", id: c && c.key ? c.key : undefined },
|
||||||
own && c.key
|
own && c.key
|
||||||
? form({ method: "POST", action: `/comments/delete/${encodeURIComponent(c.key)}`, class: "comment-delete-form" },
|
? form({ method: "POST", action: `/comments/delete/${encodeURIComponent(c.key)}`, class: "comment-delete-form" },
|
||||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, "✕"))
|
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, renderIcon("close")))
|
||||||
|
: null,
|
||||||
|
!own && c.key
|
||||||
|
? form({ method: "POST", action: `/hide/${encodeURIComponent(c.key)}`, class: "comment-delete-form" },
|
||||||
|
input({ type: "hidden", name: "returnTo", value: returnTo || (rootId ? `/thread/${encodeURIComponent(rootId)}` : "/activity") }),
|
||||||
|
input({ type: "hidden", name: "label", value: String(text).replace(/\s+/g, " ").trim().slice(0, 80) }),
|
||||||
|
button({ type: "submit", class: "btn-singleview btn-hide", title: i18n.hideContent }, renderHideIcon()))
|
||||||
: null,
|
: null,
|
||||||
span({ class: "created-at" },
|
span({ class: "created-at" },
|
||||||
author ? userLink(author) : span("(unknown)"),
|
author ? userLink(author) : span("(unknown)"),
|
||||||
|
|
@ -52,7 +58,7 @@ const renderCommentsSection = ({ action, comments = [], returnTo = null, closedN
|
||||||
div({ class: "vote-comments-section" },
|
div({ class: "vote-comments-section" },
|
||||||
list.length
|
list.length
|
||||||
? div({ class: "comments-list" }, ...list.map((c, i) => {
|
? div({ class: "comments-list" }, ...list.map((c, i) => {
|
||||||
const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null);
|
const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null, returnTo);
|
||||||
return i === list.length - 1 ? div({ id: "comments-latest", class: "comments-latest-anchor" }, card) : card;
|
return i === list.length - 1 ? div({ id: "comments-latest", class: "comments-latest-anchor" }, card) : card;
|
||||||
}))
|
}))
|
||||||
: (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)),
|
: (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe");
|
const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n, userLink, renderModuleStats, paged } = require("./main_views");
|
const { template, i18n, userLink, renderModuleStats, paged, renderIcon } = require("./main_views");
|
||||||
|
|
||||||
const KIND_LABEL = {
|
const KIND_LABEL = {
|
||||||
inhabitants: () => i18n.dataKindInhabitants,
|
inhabitants: () => i18n.dataKindInhabitants,
|
||||||
|
|
@ -32,7 +32,8 @@ const KIND_LABEL = {
|
||||||
mailing: () => i18n.mailingTitle,
|
mailing: () => i18n.mailingTitle,
|
||||||
logistics: () => i18n.logisticsTitle,
|
logistics: () => i18n.logisticsTitle,
|
||||||
podcasts: () => i18n.podcastsTitle,
|
podcasts: () => i18n.podcastsTitle,
|
||||||
campaigns: () => i18n.campaignsTitle
|
campaigns: () => i18n.campaignsTitle,
|
||||||
|
blogs: () => i18n.blogTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
const REASON_LABEL = {
|
const REASON_LABEL = {
|
||||||
|
|
@ -54,39 +55,50 @@ const FILTER_COLUMNS = [
|
||||||
["INHABITANTS", "TRIBES"],
|
["INHABITANTS", "TRIBES"],
|
||||||
["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS", "EMERGENCIES", "MAILING", "CAMPAIGNS"],
|
["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS", "EMERGENCIES", "MAILING", "CAMPAIGNS"],
|
||||||
["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY", "LOGISTICS"],
|
["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY", "LOGISTICS"],
|
||||||
["FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"],
|
["BLOGS", "FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"],
|
||||||
["AUDIOS", "BOOKMARKS", "DOCUMENTS", "FILES", "IMAGES", "TORRENTS", "VIDEOS", "PODCASTS"]
|
["AUDIOS", "BOOKMARKS", "DOCUMENTS", "FILES", "IMAGES", "TORRENTS", "VIDEOS", "PODCASTS"]
|
||||||
];
|
];
|
||||||
|
|
||||||
const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase());
|
const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase());
|
||||||
|
|
||||||
const reasonButton = (value, label, current, q) =>
|
const reasonButton = (value, label, current, filter) =>
|
||||||
form({ method: "GET", action: "/data" },
|
form({ method: "GET", action: "/data" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
filter && filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null,
|
||||||
value ? input({ type: "hidden", name: "reason", value }) : null,
|
value ? input({ type: "hidden", name: "reason", value }) : null,
|
||||||
button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase())
|
button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterButton = (value, label, current, reason) =>
|
||||||
|
form({ method: "GET", action: "/data" },
|
||||||
|
input({ type: "hidden", name: "filter", value }),
|
||||||
|
reason ? input({ type: "hidden", name: "reason", value: reason }) : null,
|
||||||
|
button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase())
|
||||||
|
);
|
||||||
|
|
||||||
const sectionId = (kind) => `data-${String(kind || '').toLowerCase()}`;
|
const sectionId = (kind) => `data-${String(kind || '').toLowerCase()}`;
|
||||||
|
|
||||||
const renderFilters = (q, payload = {}, kindsPresent = []) => {
|
const renderFilters = (q, payload = {}, kindsPresent = []) => {
|
||||||
const matches = Array.isArray(payload.matches) ? payload.matches : [];
|
const matches = Array.isArray(payload.matches) ? payload.matches : [];
|
||||||
const reason = String(payload.reason || "");
|
const reason = String(payload.reason || "");
|
||||||
if (!matches.length && !String(q || "").trim() && !reason) return null;
|
const filter = String(payload.filter || "ALL").toUpperCase();
|
||||||
|
const isKindFilter = !FILTER_COLUMNS[0].includes(filter);
|
||||||
|
if (!matches.length && !String(q || "").trim() && !reason && !isKindFilter) return null;
|
||||||
const avail = (Array.isArray(payload.reasonsAvail) ? payload.reasonsAvail : []).filter(r => REASON_LABEL[r]);
|
const avail = (Array.isArray(payload.reasonsAvail) ? payload.reasonsAvail : []).filter(r => REASON_LABEL[r]);
|
||||||
const present = new Set(kindsPresent);
|
const present = new Set(kindsPresent);
|
||||||
const columns = FILTER_COLUMNS.slice(1)
|
const columns = [FILTER_COLUMNS[0]].concat(
|
||||||
.map(col => col.filter(k => present.has(k.toLowerCase())))
|
FILTER_COLUMNS.slice(1)
|
||||||
.filter(col => col.length);
|
.map(col => col.filter(k => present.has(k.toLowerCase())))
|
||||||
|
.filter(col => col.length)
|
||||||
|
);
|
||||||
return section(
|
return section(
|
||||||
div({ class: "activity-sub-filter" },
|
div({ class: "activity-sub-filter" },
|
||||||
reasonButton("", i18n.dataFilterAll, reason, q),
|
reasonButton("", i18n.dataFilterAll, reason, filter),
|
||||||
...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, q))
|
...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, filter))
|
||||||
),
|
),
|
||||||
reason || !columns.length ? null : div({ class: "activity-filter-grid" },
|
div({ class: "activity-filter-grid" },
|
||||||
...columns.map(col =>
|
...columns.map(col =>
|
||||||
div({ class: "activity-filter-col" },
|
div({ class: "activity-filter-col" },
|
||||||
...col.map(k => a({ href: `#${sectionId(k)}`, class: "filter-btn" }, String(kindLabel(k.toLowerCase())).toUpperCase()))
|
...col.map(k => filterButton(k, FILTER_COLUMNS[0].includes(k) ? (i18n[k + "Button"] || k) : kindLabel(k.toLowerCase()), filter, reason))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -94,6 +106,7 @@ const renderFilters = (q, payload = {}, kindsPresent = []) => {
|
||||||
renderModuleStats(matches.length),
|
renderModuleStats(matches.length),
|
||||||
form({ method: "GET", action: "/data", class: "filter-box" },
|
form({ method: "GET", action: "/data", class: "filter-box" },
|
||||||
reason ? input({ type: "hidden", name: "reason", value: reason }) : null,
|
reason ? input({ type: "hidden", name: "reason", value: reason }) : null,
|
||||||
|
filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null,
|
||||||
input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }),
|
input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }),
|
||||||
div({ class: "filter-box__controls" },
|
div({ class: "filter-box__controls" },
|
||||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||||
|
|
@ -165,7 +178,7 @@ const renderMatchRow = (m, isBest) => {
|
||||||
span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()),
|
span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()),
|
||||||
span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `),
|
span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `),
|
||||||
span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`),
|
span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`),
|
||||||
a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, "↗")
|
a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, renderIcon("open"))
|
||||||
),
|
),
|
||||||
a({ href: m.href, class: "data-card-title" }, shortTitle(m.title) || shortId(m.id)),
|
a({ href: m.href, class: "data-card-title" }, shortTitle(m.title) || shortId(m.id)),
|
||||||
span({ class: "data-card-author" }, userLink(m.author)),
|
span({ class: "data-card-author" }, userLink(m.author)),
|
||||||
|
|
@ -210,12 +223,12 @@ exports.dataView = async (payload = {}) => {
|
||||||
return template(
|
return template(
|
||||||
i18n.dataTitle,
|
i18n.dataTitle,
|
||||||
section(div({ class: "tags-header module-header-line" }, h2(i18n.dataTitle), p(i18n.dataDescription))),
|
section(div({ class: "tags-header module-header-line" }, h2(i18n.dataTitle), p(i18n.dataDescription))),
|
||||||
renderFilters(q, { ...payload, matches }, groups.map(g => g.kind)),
|
renderFilters(q, { ...payload, matches }, payload.kindsMatched && typeof payload.kindsMatched === "object" ? Object.keys(payload.kindsMatched) : groups.map(g => g.kind)),
|
||||||
emptyData ? null : (payload.cohesion ? renderCohesion(payload.cohesion) : null),
|
emptyData ? null : (payload.cohesion ? renderCohesion(payload.cohesion) : null),
|
||||||
section(
|
section(
|
||||||
emptyData ? null : div({ class: "tags-header" },
|
emptyData ? null : div({ class: "tags-header" },
|
||||||
h2(q ? `${i18n.dataTopicTitle} #${q}` : i18n.dataMatchesTitle),
|
h2(i18n.dataMatchesTitle),
|
||||||
p(q ? i18n.dataTopicHint : i18n.dataMatchesHint)
|
p(i18n.dataMatchesHint)
|
||||||
),
|
),
|
||||||
best
|
best
|
||||||
? div({ class: "data-section data-top-best" },
|
? div({ class: "data-section data-top-best" },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
||||||
|
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText");
|
const { renderStyledText } = require("../backend/renderStyledText");
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para
|
||||||
div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])),
|
div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])),
|
||||||
doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null,
|
doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null,
|
||||||
doc?.url
|
doc?.url
|
||||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) })
|
||||||
: p(i18n.documentNoFile),
|
: p(i18n.documentNoFile),
|
||||||
...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []),
|
...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []),
|
||||||
engagement,
|
engagement,
|
||||||
|
|
@ -180,7 +180,6 @@ exports.documentView = async (documents, filter = "all", documentId = null, para
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||||
|
|
@ -285,7 +284,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
||||||
const docMain = div({ class: "tribe-main" },
|
const docMain = div({ class: "tribe-main" },
|
||||||
detailActions,
|
detailActions,
|
||||||
doc?.url
|
doc?.url
|
||||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) })
|
||||||
: p(i18n.documentNoFile),
|
: p(i18n.documentNoFile),
|
||||||
(() => {
|
(() => {
|
||||||
const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN;
|
const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN;
|
||||||
|
|
@ -321,7 +320,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
||||||
section(
|
section(
|
||||||
div({ class: "filters" },
|
div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/emergencies", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/emergencies", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
||||||
...chipButtons(SEVERITIES, filter, census, sevLabel),
|
...chipButtons(SEVERITIES, filter, census, sevLabel),
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts =
|
||||||
const signerId = feed.value.author || "";
|
const signerId = feed.value.author || "";
|
||||||
const refeedsNum = Number(content.refeeds || 0) || 0;
|
const refeedsNum = Number(content.refeeds || 0) || 0;
|
||||||
const commentCount = Number(content.commentCount || 0);
|
const commentCount = Number(content.commentCount || 0);
|
||||||
const styledNodes = renderStyledText(safeText, { zoomImages: true });
|
const styledNodes = renderStyledText(safeText, { zoomImages: true, ...(typeof opts.hashtagHref === 'function' ? { hashtagHref: opts.hashtagHref } : {}) });
|
||||||
const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`;
|
const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`;
|
||||||
const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me));
|
const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me));
|
||||||
const headerActions = opts.headerActions !== undefined
|
const headerActions = opts.headerActions !== undefined
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ const renderFileSeeds = (t, spreadMap) => {
|
||||||
const info = spreadInfoOf(t, spreadMap);
|
const info = spreadInfoOf(t, spreadMap);
|
||||||
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
||||||
};
|
};
|
||||||
const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||||
|
|
||||||
const renderFileTable = exports.renderFileTable = (files, filter, params = {}) => {
|
const renderFileTable = exports.renderFileTable = (files, filter, params = {}) => {
|
||||||
const returnTo = buildReturnTo(filter, params);
|
const returnTo = buildReturnTo(filter, params);
|
||||||
|
|
@ -86,18 +86,16 @@ const renderFileTable = exports.renderFileTable = (files, filter, params = {}) =
|
||||||
{ border: "1", class: "file-table" },
|
{ border: "1", class: "file-table" },
|
||||||
tr(
|
tr(
|
||||||
th(i18n.createdAt || "DATE"),
|
th(i18n.createdAt || "DATE"),
|
||||||
th(i18n.authorLabel || "AUTHOR"),
|
|
||||||
th(i18n.fileTitleLabel || "TITLE"),
|
th(i18n.fileTitleLabel || "TITLE"),
|
||||||
th(i18n.fileSizeLabel || "SIZE"),
|
th(i18n.fileSizeLabel || "SIZE"),
|
||||||
th(i18n.seedsLabel),
|
th({ class: "torrent-spread-cell" }, i18n.seedsLabel),
|
||||||
th(i18n.seedAction),
|
th({ class: "torrent-spread-cell" }, i18n.seedAction),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
files.map((t) =>
|
files.map((t) =>
|
||||||
tr(
|
tr(
|
||||||
td({ class: "row-chips" }, t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('files', t.title, t.key)) : null),
|
td({ class: "row-chips" }, t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('files', t.title, t.key)) : null),
|
||||||
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||||
td({ class: "row-author" }, userLink(t.author)),
|
|
||||||
td({ class: "row-title", "data-label": i18n.fileTitleLabel || "TITLE" }, t.title || ""),
|
td({ class: "row-title", "data-label": i18n.fileTitleLabel || "TITLE" }, t.title || ""),
|
||||||
td({ "data-label": i18n.fileSizeLabel || "SIZE" }, formatSize(t.size)),
|
td({ "data-label": i18n.fileSizeLabel || "SIZE" }, formatSize(t.size)),
|
||||||
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, renderFileSeeds(t, params.spreadMap)),
|
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, renderFileSeeds(t, params.spreadMap)),
|
||||||
|
|
@ -226,7 +224,6 @@ exports.filesView = async (files, filter = "all", fileId = null, params = {}) =>
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
||||||
|
|
@ -388,7 +385,7 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params =
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
renderEngagement(fileObj.key,
|
fileObj.tribeId ? null : renderEngagement(fileObj.key,
|
||||||
renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants),
|
renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants),
|
||||||
renderFileCommentsSection(fileObj.key, comments, returnTo)
|
renderFileCommentsSection(fileObj.key, comments, returnTo)
|
||||||
)
|
)
|
||||||
|
|
@ -405,7 +402,6 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params =
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,12 @@ const buildGraphSvg = (me, peers, links = []) => {
|
||||||
+ `</svg>`;
|
+ `</svg>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomableGraph = (id, svg) => [
|
const zoomableGraph = (id, svg, stats = []) => [
|
||||||
div({ class: 'graphos-zoom-row' }, a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`)),
|
div({ class: 'graphos-legend graphos-statsbar' }, ...stats.filter(Boolean)),
|
||||||
div({ class: 'graphos-canvas', [RAW_HTML]: svg }),
|
div({ class: 'graphos-canvas' },
|
||||||
|
a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`),
|
||||||
|
div({ class: 'graphos-canvas-svg', [RAW_HTML]: svg })
|
||||||
|
),
|
||||||
div({ id, class: 'lightbox graphos-lightbox' },
|
div({ id, class: 'lightbox graphos-lightbox' },
|
||||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||||
div({ class: 'graphos-canvas-zoom' },
|
div({ class: 'graphos-canvas-zoom' },
|
||||||
|
|
@ -108,7 +111,7 @@ const zoomableGraph = (id, svg) => [
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
const kpi = (label, value) => div({ class: 'stats-kpi stats-kpi-inline' },
|
const kpi = (label, value) => span({ class: 'graphos-legend-item graphos-kpi' },
|
||||||
span({ class: 'stats-kpi-label' }, String(label).toUpperCase()),
|
span({ class: 'stats-kpi-label' }, String(label).toUpperCase()),
|
||||||
span({ class: 'stats-kpi-value' }, String(value))
|
span({ class: 'stats-kpi-value' }, String(value))
|
||||||
);
|
);
|
||||||
|
|
@ -153,15 +156,12 @@ exports.graphosView = ({ filter, me, peers, links = [], kpis, focus = null, focu
|
||||||
capped
|
capped
|
||||||
? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`)
|
? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`)
|
||||||
: null,
|
: null,
|
||||||
div({ class: 'stats-block graphos-stats' },
|
...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links), [
|
||||||
div({ class: 'stats-grid' },
|
kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total),
|
||||||
kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total),
|
kpi(i18n.online || 'Online', kpis.online),
|
||||||
kpi(i18n.online || 'Online', kpis.online),
|
filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null,
|
||||||
filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null,
|
filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null
|
||||||
filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null
|
]),
|
||||||
)
|
|
||||||
),
|
|
||||||
...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links)),
|
|
||||||
div({ class: 'graphos-legend' },
|
div({ class: 'graphos-legend' },
|
||||||
legendItem('me', i18n.graphosYou || 'You'),
|
legendItem('me', i18n.graphosYou || 'You'),
|
||||||
legendItem('online', i18n.online || 'Online'),
|
legendItem('online', i18n.online || 'Online'),
|
||||||
|
|
@ -267,18 +267,15 @@ exports.graphosFederationView = ({ myName, rows = [], links = [], ownVersion = '
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
div({ class: 'stats-block graphos-stats' },
|
...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion), [
|
||||||
div({ class: 'stats-grid' },
|
kpi(i18n.graphosFedPubs, sorted.length),
|
||||||
kpi(i18n.graphosFedPubs, sorted.length),
|
kpi(i18n.peerStateConnected, count(r => r.state === 'connected')),
|
||||||
kpi(i18n.peerStateConnected, count(r => r.state === 'connected')),
|
kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')),
|
||||||
kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')),
|
kpi(i18n.peerOutdated, count(outdated)),
|
||||||
kpi(i18n.peerOutdated, count(outdated)),
|
kpi(i18n.peerErrTor, count(r => r.transport === 'tor')),
|
||||||
kpi(i18n.peerErrTor, count(r => r.transport === 'tor')),
|
kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length),
|
||||||
kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length),
|
kpi(i18n.graphosFedIsolated, count(r => r.isolated))
|
||||||
kpi(i18n.graphosFedIsolated, count(r => r.isolated))
|
]) : [p(i18n.graphosFedEmpty)]),
|
||||||
)
|
|
||||||
),
|
|
||||||
...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion)) : [p(i18n.graphosFedEmpty)]),
|
|
||||||
div({ class: 'graphos-legend' },
|
div({ class: 'graphos-legend' },
|
||||||
legendItem('me', i18n.graphosYou || 'You'),
|
legendItem('me', i18n.graphosYou || 'You'),
|
||||||
span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-connected' }), span(i18n.peerStateConnected)),
|
span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-connected' }), span(i18n.peerStateConnected)),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
||||||
|
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText")
|
const { renderStyledText } = require("../backend/renderStyledText")
|
||||||
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||||
|
|
@ -36,7 +36,7 @@ const renderTags = (tags) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderImageMedia = (imgObj, filter, params = {}) => {
|
const renderImageMedia = (imgObj, filter, params = {}) => {
|
||||||
const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : "";
|
const src = imgObj?.url ? blobUrlOf(imgObj.url) : "";
|
||||||
|
|
||||||
return imgObj?.url
|
return imgObj?.url
|
||||||
? div(
|
? div(
|
||||||
|
|
@ -144,7 +144,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => {
|
||||||
br(),
|
br(),
|
||||||
br(),
|
br(),
|
||||||
imageToEdit?.url
|
imageToEdit?.url
|
||||||
? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(imageToEdit.url)}`, class: "media-preview", alt: imageToEdit?.title || "" })
|
? img({ loading: 'lazy', src: blobUrlOf(imageToEdit.url), class: "media-preview", alt: imageToEdit?.title || "" })
|
||||||
: null,
|
: null,
|
||||||
label(i18n.imageTitleLabel),
|
label(i18n.imageTitleLabel),
|
||||||
br(),
|
br(),
|
||||||
|
|
@ -188,7 +188,7 @@ const renderGallery = (images) => {
|
||||||
|
|
||||||
const renderLightbox = (images) =>
|
const renderLightbox = (images) =>
|
||||||
images.map((imgObj) => {
|
images.map((imgObj) => {
|
||||||
const src = imgObj.url ? `/blob/${encodeURIComponent(imgObj.url)}` : "";
|
const src = imgObj.url ? blobUrlOf(imgObj.url) : "";
|
||||||
return div(
|
return div(
|
||||||
{ id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" },
|
{ id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" },
|
||||||
a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "×"),
|
a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "×"),
|
||||||
|
|
@ -239,7 +239,6 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {})
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()),
|
||||||
|
|
@ -351,7 +350,7 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
||||||
? div(
|
? div(
|
||||||
{ class: "image-container" },
|
{ class: "image-container" },
|
||||||
img({
|
img({
|
||||||
src: `/blob/${encodeURIComponent(imageObj.url)}`,
|
src: blobUrlOf(imageObj.url),
|
||||||
alt: imageObj.title || "",
|
alt: imageObj.title || "",
|
||||||
class: "media-preview",
|
class: "media-preview",
|
||||||
loading: "lazy"
|
loading: "lazy"
|
||||||
|
|
@ -392,7 +391,6 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -160,11 +160,6 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
||||||
fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : ''
|
fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : ''
|
||||||
};
|
};
|
||||||
return div({ class: 'trending-card inhabitants-card' + (isMe ? ' own-content' : '') },
|
return div({ class: 'trending-card inhabitants-card' + (isMe ? ' own-content' : '') },
|
||||||
div(
|
|
||||||
{ class: 'card-header activity-card-header' },
|
|
||||||
span(),
|
|
||||||
renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`, { author: user.id })
|
|
||||||
),
|
|
||||||
div({ class: 'card-section inhabitants-card-body' },
|
div({ class: 'card-section inhabitants-card-body' },
|
||||||
div({ class: 'inhabitant-card' },
|
div({ class: 'inhabitant-card' },
|
||||||
div({ class: 'inhabitant-left' },
|
div({ class: 'inhabitant-left' },
|
||||||
|
|
@ -175,7 +170,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
||||||
user.name ? p({ class: 'inhabitant-left-name' }, userLink(user.id, user.name)) : null,
|
user.name ? p({ class: 'inhabitant-left-name' }, userLink(user.id, user.name)) : null,
|
||||||
...renderUserSensors({
|
...renderUserSensors({
|
||||||
isMe, fediverseConfigured, prefs, id: user.id,
|
isMe, fediverseConfigured, prefs, id: user.id,
|
||||||
karmaScore: user.karmaScore, carbonGrams: user.carbonGrams,
|
karmaScore: user.karmaScore, karmaReceived: user.karmaReceived || null, carbonGrams: user.carbonGrams,
|
||||||
deviceSource: user.deviceSource, activityBucket: user.lastActivityBucket,
|
deviceSource: user.deviceSource, activityBucket: user.lastActivityBucket,
|
||||||
gpgFingerprint: user.gpgFingerprint, ecoAddress: user.ecoAddress,
|
gpgFingerprint: user.gpgFingerprint, ecoAddress: user.ecoAddress,
|
||||||
estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed,
|
estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed,
|
||||||
|
|
@ -411,7 +406,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
||||||
safe.oasisVersion ? div({ class: 'profile-side-version' }, '🌴 ' + (i18n.oasisVersionLabel || 'Oasis Version') + ': ', strong(safe.oasisVersion)) : null,
|
safe.oasisVersion ? div({ class: 'profile-side-version' }, '🌴 ' + (i18n.oasisVersionLabel || 'Oasis Version') + ': ', strong(safe.oasisVersion)) : null,
|
||||||
...renderUserSensors({
|
...renderUserSensors({
|
||||||
isMe, fediverseConfigured, prefs, id: id || viewedId,
|
isMe, fediverseConfigured, prefs, id: id || viewedId,
|
||||||
karmaScore, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket,
|
karmaScore, karmaReceived: safe.karmaReceived || null, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket,
|
||||||
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
||||||
larpHouse, stats: safe.stats,
|
larpHouse, stats: safe.stats,
|
||||||
mutual: !!(safe.relationship && safe.relationship.following && safe.relationship.followsMe)
|
mutual: !!(safe.relationship && safe.relationship.following && safe.relationship.followsMe)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe");
|
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText");
|
const { renderStyledText } = require("../backend/renderStyledText");
|
||||||
const { template, i18n, userLink, paged } = require("./main_views");
|
const { template, i18n, userLink, paged, renderIcon } = require("./main_views");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
|
|
||||||
const userId = config.keys.id;
|
const userId = config.keys.id;
|
||||||
|
|
@ -209,7 +209,7 @@ const renderPostsBlock = (posts, house, canPost) => {
|
||||||
paged(stream).map(entry => div({ class: entry.post.author === userId ? 'larp-post own-comment' : 'larp-post' },
|
paged(stream).map(entry => div({ class: entry.post.author === userId ? 'larp-post own-comment' : 'larp-post' },
|
||||||
entry.post.author === userId && entry.post.id
|
entry.post.author === userId && entry.post.id
|
||||||
? form({ method: 'POST', action: `/larp/post/delete/${encodeURIComponent(entry.post.id)}`, class: 'comment-delete-form' },
|
? form({ method: 'POST', action: `/larp/post/delete/${encodeURIComponent(entry.post.id)}`, class: 'comment-delete-form' },
|
||||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, '✕'))
|
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close')))
|
||||||
: null,
|
: null,
|
||||||
div({ class: 'larp-post-head' },
|
div({ class: 'larp-post-head' },
|
||||||
userLink(entry.post.author),
|
userLink(entry.post.author),
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,6 @@ const renderFilters = (filter, q, zone, census, zones, emptyMod) =>
|
||||||
section(
|
section(
|
||||||
div({ class: "filters" },
|
div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/logistics", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/logistics", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
input({ type: "hidden", name: "zone", value: zone || "" }),
|
input({ type: "hidden", name: "zone", value: zone || "" }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
||||||
section(
|
section(
|
||||||
div({ class: "filters" },
|
div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/mailing", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/mailing", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
||||||
...chipButtons(TYPE_FILTERS, filter, census, typeLabel),
|
...chipButtons(TYPE_FILTERS, filter, census, typeLabel),
|
||||||
|
|
|
||||||
|
|
@ -111,18 +111,27 @@ const renderLiveFeed = (src, imgClass) => {
|
||||||
a({ href: `#${id}`, id: `${id}-src`, class: 'zoom-link zoom-inline' }, img({ src, alt: '', class: imgClass })),
|
a({ href: `#${id}`, id: `${id}-src`, class: 'zoom-link zoom-inline' }, img({ src, alt: '', class: imgClass })),
|
||||||
div({ id, class: 'lightbox live-lightbox' },
|
div({ id, class: 'lightbox live-lightbox' },
|
||||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||||
|
a({ href: `#${id}-full`, class: 'live-fullscreen-btn', title: i18n.liveFullscreen }, renderIcon('fullscreen')),
|
||||||
a({ href: `#${id}-src`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' }))
|
a({ href: `#${id}-src`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' }))
|
||||||
|
),
|
||||||
|
div({ id: `${id}-full`, class: 'lightbox live-lightbox live-fullscreen' },
|
||||||
|
a({ href: `#${id}`, class: 'lightbox-close' }, '\u00d7'),
|
||||||
|
a({ href: `#${id}`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' }))
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
exports.renderLiveFeed = renderLiveFeed;
|
exports.renderLiveFeed = renderLiveFeed;
|
||||||
|
|
||||||
const renderVideoGrid = (peers, me) => {
|
const renderVideoGrid = (peers, me, { film = false } = {}) => {
|
||||||
|
const list = (Array.isArray(peers) ? peers : []).filter(p => p && p.id);
|
||||||
|
const filmName = (...label) => span({ class: 'video-tile-name video-tile-film' }, '▶ ', ...label);
|
||||||
const tiles = [
|
const tiles = [
|
||||||
me ? div({ class: 'video-tile video-tile-me' }, ...renderLiveFeed('/phone/video/me', 'video-feed'), span({ class: 'video-tile-name' }, i18n.roomYou)) : null,
|
me ? div({ class: 'video-tile video-tile-me' }, ...renderLiveFeed('/phone/video/me', 'video-feed'), span({ class: 'video-tile-name' }, i18n.roomYou)) : null,
|
||||||
...(Array.isArray(peers) ? peers : []).filter(p => p && p.id && p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id)))
|
film ? div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent('film:me')}`, 'video-feed'), filmName(i18n.roomYou)) : null,
|
||||||
|
...list.filter(p => p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id))),
|
||||||
|
...list.filter(p => p.film).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(`film:${p.id}`)}`, 'video-feed'), filmName(userLink(p.id))))
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return tiles.length ? div({ class: 'video-grid' }, ...tiles) : null;
|
return tiles.length ? div({ class: tiles.length === 1 ? 'video-grid video-grid-solo' : 'video-grid' }, ...tiles) : null;
|
||||||
};
|
};
|
||||||
exports.renderVideoGrid = renderVideoGrid;
|
exports.renderVideoGrid = renderVideoGrid;
|
||||||
|
|
||||||
|
|
@ -148,7 +157,7 @@ const renderSubscriptionBox = ({ target, scope, subscribed, count, isOwner, retu
|
||||||
: button({ type: 'submit', class: subscribed ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, `${subscribed ? '🔕' : '🔔'} ${String(subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()}`)
|
: button({ type: 'submit', class: subscribed ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, `${subscribed ? '🔕' : '🔔'} ${String(subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()}`)
|
||||||
),
|
),
|
||||||
showPm && total
|
showPm && total
|
||||||
? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, '✉')
|
? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, renderIcon('mail'))
|
||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
@ -254,7 +263,7 @@ const renderLinkBox = (blockId, viewHref, title) => {
|
||||||
const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`;
|
const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`;
|
||||||
const clearnetHref = clearnetHrefFor(viewHref, blockId, title);
|
const clearnetHref = clearnetHrefFor(viewHref, blockId, title);
|
||||||
return [
|
return [
|
||||||
a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, '⚯'),
|
a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')),
|
||||||
span({ id, class: 'lightbox link-lightbox' },
|
span({ id, class: 'lightbox link-lightbox' },
|
||||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||||
span({ class: 'link-box' },
|
span({ class: 'link-box' },
|
||||||
|
|
@ -297,6 +306,29 @@ const renderFileDownloads = (fileKey, torrentUrl, name, opts = {}) => {
|
||||||
};
|
};
|
||||||
exports.renderFileDownloads = renderFileDownloads;
|
exports.renderFileDownloads = renderFileDownloads;
|
||||||
|
|
||||||
|
const blobUrlOf = (v) => (String(v || '').startsWith('/blob/sealed/') ? String(v) : `/blob/${encodeURIComponent(v)}`);
|
||||||
|
exports.blobUrlOf = blobUrlOf;
|
||||||
|
const ACTION_ICONS = {
|
||||||
|
hide: '<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/>',
|
||||||
|
spread: '<path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/>',
|
||||||
|
pin: '<path d="M9 3h6l-1 6 4 4H6l4-4-1-6z"/><line x1="12" y1="13" x2="12" y2="21"/>',
|
||||||
|
report: '<line x1="5" y1="21" x2="5" y2="3"/><path d="M5 4h12l-2.5 4.5L17 13H5"/>',
|
||||||
|
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3 7 12 13 21 7"/>',
|
||||||
|
call: '<path d="M5 3h4l2 5-2.5 1.5a11 11 0 0 0 6 6L16 13l5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 5a2 2 0 0 1 2-2z"/>',
|
||||||
|
link: '<path d="M10 14a4.5 4.5 0 0 0 6.4 0l3.2-3.2a4.5 4.5 0 0 0-6.4-6.4L11.8 5.8"/><path d="M14 10a4.5 4.5 0 0 0-6.4 0l-3.2 3.2a4.5 4.5 0 0 0 6.4 6.4l1.4-1.4"/>',
|
||||||
|
block: '<path d="M12 2.5l8.5 4.75v9.5L12 21.5l-8.5-4.75v-9.5z"/><polyline points="3.5 7.25 12 12 20.5 7.25"/><line x1="12" y1="12" x2="12" y2="21.5"/>',
|
||||||
|
datagram: '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/>',
|
||||||
|
open: '<path d="M7 17L17 7M8 7h9v9"/>',
|
||||||
|
donate: '<line x1="12" y1="2" x2="12" y2="22"/><line x1="3.3" y1="7" x2="20.7" y2="17"/><line x1="3.3" y1="17" x2="20.7" y2="7"/><polyline points="9.5 4 12 6 14.5 4"/><polyline points="9.5 20 12 18 14.5 20"/>',
|
||||||
|
torrent: '<line x1="7" y1="20" x2="7" y2="4"/><polyline points="3 8 7 4 11 8"/><line x1="17" y1="4" x2="17" y2="20"/><polyline points="13 16 17 20 21 16"/>',
|
||||||
|
close: '<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>',
|
||||||
|
film: '<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="3" y1="12" x2="21" y2="12"/>',
|
||||||
|
fullscreen: '<polyline points="4 9 4 4 9 4"/><polyline points="15 4 20 4 20 9"/><polyline points="20 15 20 20 15 20"/><polyline points="9 20 4 20 4 15"/>'
|
||||||
|
};
|
||||||
|
const renderIcon = (name) => span({ class: 'btn-icon', [RAW_HTML]: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ACTION_ICONS[name] || ''}</svg>` });
|
||||||
|
const renderHideIcon = () => renderIcon('hide');
|
||||||
|
exports.renderIcon = renderIcon;
|
||||||
|
exports.renderHideIcon = renderHideIcon;
|
||||||
const renderContentActions = (msgId, viewHref, opts = {}) => {
|
const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
const o = (opts && typeof opts === 'object') ? opts : {};
|
const o = (opts && typeof opts === 'object') ? opts : {};
|
||||||
const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null;
|
const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null;
|
||||||
|
|
@ -306,7 +338,7 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
const pinBtn = o.favKind && blockId
|
const pinBtn = o.favKind && blockId
|
||||||
? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
||||||
o.returnTo ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
o.returnTo ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
||||||
button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, o.isFavorite ? '✜' : '✛')
|
button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, renderIcon('pin'))
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -318,17 +350,17 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
if (!src || typeof src.blobId !== 'string' || !src.blobId.startsWith('&')) return null;
|
if (!src || typeof src.blobId !== 'string' || !src.blobId.startsWith('&')) return null;
|
||||||
try { if (getConfig().modules.torrentsMod !== 'on') return null; } catch (_) { return null; }
|
try { if (getConfig().modules.torrentsMod !== 'on') return null; } catch (_) { return null; }
|
||||||
const name = String(src.name || o.reportTitle || '').slice(0, 100);
|
const name = String(src.name || o.reportTitle || '').slice(0, 100);
|
||||||
return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, '\u21C5');
|
return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, renderIcon('torrent'));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const chainBtn = blockId && process.env.OASIS_MOBILE !== '1'
|
const chainBtn = blockId && process.env.OASIS_MOBILE !== '1'
|
||||||
? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, '⦿')
|
? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, renderIcon('block'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const herePath = (listScope() || {}).path;
|
const herePath = (listScope() || {}).path;
|
||||||
const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })();
|
const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })();
|
||||||
const contentBtn = viewHref && !onItself
|
const contentBtn = viewHref && !onItself
|
||||||
? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, '↗')
|
? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, renderIcon('open'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const donateBtn = (() => {
|
const donateBtn = (() => {
|
||||||
|
|
@ -337,32 +369,41 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
const donatable = sharedState.getDonatableAuthors ? sharedState.getDonatableAuthors() : null;
|
const donatable = sharedState.getDonatableAuthors ? sharedState.getDonatableAuthors() : null;
|
||||||
if (!donatable || !donatable.has(String(o.author))) return null;
|
if (!donatable || !donatable.has(String(o.author))) return null;
|
||||||
const ref = viewHref ? `?returnTo=${encodeURIComponent(viewHref)}` : '';
|
const ref = viewHref ? `?returnTo=${encodeURIComponent(viewHref)}` : '';
|
||||||
return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, '\u2744');
|
return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, renderIcon('donate'));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const reportHref = blockId
|
const reportHref = blockId
|
||||||
? `/reports?filter=create&category=CONTENT&title=${encodeURIComponent(String(o.reportTitle || blockId).slice(0, 120))}&description=${encodeURIComponent(`${viewHref || blockId}`)}`
|
? `/reports?filter=create&category=CONTENT&title=${encodeURIComponent(String(o.reportTitle || blockId).slice(0, 120))}&description=${encodeURIComponent(`${viewHref || blockId}`)}`
|
||||||
: null;
|
: null;
|
||||||
const reportBtn = o.report !== false && reportHref && !(o.author && String(o.author) === String(myId))
|
const reportBtn = o.report !== false && reportHref && !(o.author && String(o.author) === String(myId))
|
||||||
? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, '⚑')
|
? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, renderIcon('report'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const pmBtn = o.author && String(o.author) !== String(myId)
|
const pmBtn = o.contact === true && o.author && String(o.author) !== String(myId)
|
||||||
? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, '✉')
|
? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, renderIcon('mail'))
|
||||||
: null;
|
: null;
|
||||||
const callBtn = o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author)
|
const callBtn = o.contact === true && o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author)
|
||||||
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, '✆')
|
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const deleteBtn = o.deleteAction
|
const deleteBtn = o.deleteAction
|
||||||
? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' },
|
? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' },
|
||||||
o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
||||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, '✕')
|
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close'))
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn) return null;
|
const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId))
|
||||||
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
||||||
|
input({ type: 'hidden', name: 'returnTo', value: o.returnTo || viewHref || '/' }),
|
||||||
|
viewHref ? input({ type: 'hidden', name: 'href', value: viewHref }) : null,
|
||||||
|
o.reportTitle ? input({ type: 'hidden', name: 'label', value: String(o.reportTitle).slice(0, 120) }) : null,
|
||||||
|
button({ type: 'submit', class: 'btn-singleview btn-hide', title: i18n.hideContent }, renderHideIcon())
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null;
|
||||||
|
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
||||||
};
|
};
|
||||||
exports.renderContentActions = renderContentActions;
|
exports.renderContentActions = renderContentActions;
|
||||||
const CONTENT_FAV_KIND = {
|
const CONTENT_FAV_KIND = {
|
||||||
|
|
@ -416,8 +457,9 @@ exports.renderDocumentActions = renderDocumentActions;
|
||||||
const renderRelationshipBlock = (relationship, feedId) => {
|
const renderRelationshipBlock = (relationship, feedId) => {
|
||||||
const rel = relationship || {};
|
const rel = relationship || {};
|
||||||
if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou);
|
if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou);
|
||||||
|
const muted = (() => { try { return require('../models/viewer_filters').isMuted(feedId); } catch (_) { return false; } })();
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺' };
|
const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺', mute: '◌', unmute: '●' };
|
||||||
const addAction = (action) => actions.push(
|
const addAction = (action) => actions.push(
|
||||||
form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' },
|
form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' },
|
||||||
button({ type: 'submit', class: 'filter-btn' }, `${icons[action]} ${i18n[action]}`)
|
button({ type: 'submit', class: 'filter-btn' }, `${icons[action]} ${i18n[action]}`)
|
||||||
|
|
@ -426,7 +468,9 @@ const renderRelationshipBlock = (relationship, feedId) => {
|
||||||
if (rel.following) addAction('unfollow');
|
if (rel.following) addAction('unfollow');
|
||||||
else if (rel.blocking) addAction('unblock');
|
else if (rel.blocking) addAction('unblock');
|
||||||
else { addAction('follow'); addAction('block'); }
|
else { addAction('follow'); addAction('block'); }
|
||||||
|
if (!rel.blocking) addAction(muted ? 'unmute' : 'mute');
|
||||||
return div({ class: 'relationship-status' },
|
return div({ class: 'relationship-status' },
|
||||||
|
muted ? span({ class: 'status blocked' }, i18n.relationshipMuted) : null,
|
||||||
rel.blocking && rel.blockedBy
|
rel.blocking && rel.blockedBy
|
||||||
? span({ class: 'status blocked' }, i18n.relationshipMutualBlock)
|
? span({ class: 'status blocked' }, i18n.relationshipMutualBlock)
|
||||||
: [
|
: [
|
||||||
|
|
@ -686,7 +730,7 @@ const renderSpreadButton = (msgKey, opts, title = null, showCount = true) => {
|
||||||
{ method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' },
|
{ method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' },
|
||||||
button(
|
button(
|
||||||
{ type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: title || i18n.spreadContent },
|
{ type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: title || i18n.spreadContent },
|
||||||
showCount ? `⟳ ${count}` : '⟳'
|
renderIcon('spread'), showCount ? span({ class: 'btn-count' }, String(count)) : null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1946,7 +1990,7 @@ const template = (titlePrefix, ...elements) => {
|
||||||
return renderEmergencyBanner(featured, { compact });
|
return renderEmergencyBanner(featured, { compact });
|
||||||
} catch (_) { return null; }
|
} catch (_) { return null; }
|
||||||
};
|
};
|
||||||
const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : "blocks";
|
const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks";
|
||||||
const themeLink = link({
|
const themeLink = link({
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: `/assets/themes/${theme}.css?v=${assetVersion()}`
|
href: `/assets/themes/${theme}.css?v=${assetVersion()}`
|
||||||
|
|
@ -2039,7 +2083,7 @@ const template = (titlePrefix, ...elements) => {
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
renderPhoneTopLink(),
|
renderPhoneTopLink(),
|
||||||
(uxMode === "chats" || uxMode === "feed" || uxMode === "phone")
|
(uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms")
|
||||||
? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
|
? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
|
||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
|
|
@ -2092,7 +2136,7 @@ const template = (titlePrefix, ...elements) => {
|
||||||
navLink({ href: "/peers", emoji: "⧖", text: i18n.peers })
|
navLink({ href: "/peers", emoji: "⧖", text: i18n.peers })
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone") ? div(
|
) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") ? div(
|
||||||
{ class: "top-bar-right" },
|
{ class: "top-bar-right" },
|
||||||
nav(
|
nav(
|
||||||
ul(
|
ul(
|
||||||
|
|
@ -2151,7 +2195,7 @@ const template = (titlePrefix, ...elements) => {
|
||||||
buildPhoneBanner(),
|
buildPhoneBanner(),
|
||||||
buildEmergencyBanner(false),
|
buildEmergencyBanner(false),
|
||||||
div(
|
div(
|
||||||
{ class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" ? "main-content chatsux-only" : "main-content" },
|
{ class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" || uxMode === "rooms" ? "main-content chatsux-only" : "main-content" },
|
||||||
uxMode !== "blocks" ? null : div(
|
uxMode !== "blocks" ? null : div(
|
||||||
{ class: "sidebar-left" },
|
{ class: "sidebar-left" },
|
||||||
nav(
|
nav(
|
||||||
|
|
@ -2868,6 +2912,15 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => {
|
||||||
p({ class: "post-text" }, ...renderStyledText(rawText))
|
p({ class: "post-text" }, ...renderStyledText(rawText))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const postTags = String(content.type || '') === 'post' && Array.isArray(content.tags) ? content.tags.filter(Boolean) : [];
|
||||||
|
if (postTags.length) {
|
||||||
|
articleElement = div(
|
||||||
|
articleElement,
|
||||||
|
div({ class: 'card-tags card-tags-mt' },
|
||||||
|
...postTags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
return section(
|
return section(
|
||||||
|
|
@ -3158,8 +3211,8 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = '
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt }) => {
|
exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt, tags = [] }) => {
|
||||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText } = require('./clearnet_view');
|
const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText, renderTagChips } = require('./clearnet_view');
|
||||||
const rawText = String(text || '');
|
const rawText = String(text || '');
|
||||||
const renderedHtml = sanitizeHtml(renderRichText(rawText));
|
const renderedHtml = sanitizeHtml(renderRichText(rawText));
|
||||||
const plainPreview = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').slice(0, 200);
|
const plainPreview = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').slice(0, 200);
|
||||||
|
|
@ -3189,6 +3242,7 @@ exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWar
|
||||||
${cw ? `<div class="cn-blog-cw">${cw}</div>` : ''}
|
${cw ? `<div class="cn-blog-cw">${cw}</div>` : ''}
|
||||||
<hr class="cn-sep"/>
|
<hr class="cn-sep"/>
|
||||||
<article class="cn-blog-body">${renderedHtml}</article>
|
<article class="cn-blog-body">${renderedHtml}</article>
|
||||||
|
${renderTagChips(tags)}
|
||||||
`;
|
`;
|
||||||
return renderClearnetPage({
|
return renderClearnetPage({
|
||||||
title: `${titleText} | Oasis`,
|
title: `${titleText} | Oasis`,
|
||||||
|
|
@ -3334,7 +3388,7 @@ const buildClearnetHub = ({ items = {}, filterBase, filterType = '', query = '',
|
||||||
exports.CLEARNET_HUB_CSS = CLEARNET_HUB_CSS;
|
exports.CLEARNET_HUB_CSS = CLEARNET_HUB_CSS;
|
||||||
|
|
||||||
exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filterType = '', query = '', page = 1, tribesPage = 1 }) => {
|
exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filterType = '', query = '', page = 1, tribesPage = 1 }) => {
|
||||||
const { escapeHtml: esc, renderClearnetPage, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view');
|
const { escapeHtml: esc, renderClearnetPage, renderClearnetHubSearch, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view');
|
||||||
const hub = buildClearnetHub({ items, filterBase: '/c', filterType, query, page });
|
const hub = buildClearnetHub({ items, filterBase: '/c', filterType, query, page });
|
||||||
const extraCss = CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS + `
|
const extraCss = CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS + `
|
||||||
`;
|
`;
|
||||||
|
|
@ -3361,7 +3415,7 @@ exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filter
|
||||||
ogDescription: i18n.cnHubDescription,
|
ogDescription: i18n.cnHubDescription,
|
||||||
extraCss,
|
extraCss,
|
||||||
body,
|
body,
|
||||||
headerExtra: `<form class="cn-search" method="GET" action="/c">${filterType ? `<input type="hidden" name="type" value="${esc(filterType)}"/>` : ''}<input type="text" name="q" value="${esc(query || '')}" placeholder="${esc(i18n.cnSearchPlaceholder)}" autocomplete="off"/></form>`
|
headerExtra: renderClearnetHubSearch({ type: filterType, query })
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3516,7 +3570,17 @@ const renderUserSensors = (u, opts = {}) => {
|
||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
const items = [];
|
const items = [];
|
||||||
if (show('karma')) items.push(span({ class: 'karma-line' }, `${i18n.bankingUserEngagementScore}: `, strong(String(u.karmaScore !== undefined ? u.karmaScore : 0))));
|
const kr = u.karmaReceived && typeof u.karmaReceived === 'object' ? u.karmaReceived : null;
|
||||||
|
const krPos = kr ? Number(kr.positive) || 0 : 0;
|
||||||
|
const krNeg = kr ? Number(kr.negative) || 0 : 0;
|
||||||
|
const krTone = !kr ? '' : krPos > krNeg ? ' karma-value-positive' : krPos < krNeg ? ' karma-value-negative' : ' karma-value-neutral';
|
||||||
|
if (show('karma')) items.push(span({ class: kr ? 'karma-line karma-line-stacked' : 'karma-line' },
|
||||||
|
span({ class: 'karma-line-main' }, `${i18n.bankingUserEngagementScore}: `, strong({ class: 'karma-value' + krTone }, String(u.karmaScore !== undefined ? u.karmaScore : 0))),
|
||||||
|
kr ? span({ class: 'karma-received-row' },
|
||||||
|
span({ class: 'karma-received karma-received-up' }, span({ class: 'karma-arrow' }, '▲'), String(krPos)),
|
||||||
|
span({ class: 'karma-received karma-received-down' }, span({ class: 'karma-arrow' }, '▼'), String(krNeg))
|
||||||
|
) : null
|
||||||
|
));
|
||||||
if (show('ecoTax')) items.push(span({ class: 'karma-line eco-tax-line' }, `${i18n.profileVisibilityEcoTax || 'ECO Tax'}: `, strong(fmtCarbon(u.carbonGrams))));
|
if (show('ecoTax')) items.push(span({ class: 'karma-line eco-tax-line' }, `${i18n.profileVisibilityEcoTax || 'ECO Tax'}: `, strong(fmtCarbon(u.carbonGrams))));
|
||||||
if (activityChip) items.push(activityChip);
|
if (activityChip) items.push(activityChip);
|
||||||
if (deviceChip) items.push(deviceChip);
|
if (deviceChip) items.push(deviceChip);
|
||||||
|
|
@ -3587,6 +3651,7 @@ exports.authorView = async ({
|
||||||
relationship,
|
relationship,
|
||||||
ecoAddress,
|
ecoAddress,
|
||||||
karmaScore = 0,
|
karmaScore = 0,
|
||||||
|
karmaReceived = null,
|
||||||
estimatedUBI = 0,
|
estimatedUBI = 0,
|
||||||
lastClaimedDate = null,
|
lastClaimedDate = null,
|
||||||
totalClaimed = 0,
|
totalClaimed = 0,
|
||||||
|
|
@ -3636,7 +3701,7 @@ exports.authorView = async ({
|
||||||
|
|
||||||
const userSensors = renderUserSensors({
|
const userSensors = renderUserSensors({
|
||||||
isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId,
|
isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId,
|
||||||
karmaScore, carbonGrams, deviceSource, activityBucket: lastActivityBucket,
|
karmaScore, karmaReceived, carbonGrams, deviceSource, activityBucket: lastActivityBucket,
|
||||||
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
||||||
larpHouse, stats,
|
larpHouse, stats,
|
||||||
mutual: !!(relationship && relationship.following && relationship.followsMe)
|
mutual: !!(relationship && relationship.following && relationship.followsMe)
|
||||||
|
|
@ -4818,7 +4883,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.threadView = ({ messages, spreadMap = null }) => {
|
exports.threadView = ({ messages, spreadMap = null, hiddenRoot = false }) => {
|
||||||
|
if (!Array.isArray(messages) || !messages.length) {
|
||||||
|
return template(i18n.hiddenByYou, section(p({ class: 'muted hidden-note' }, i18n.hiddenByYou)));
|
||||||
|
}
|
||||||
const rootMessage = messages[0];
|
const rootMessage = messages[0];
|
||||||
const rootAuthorName = rootMessage.value.meta.author.name;
|
const rootAuthorName = rootMessage.value.meta.author.name;
|
||||||
|
|
||||||
|
|
@ -4829,6 +4897,7 @@ exports.threadView = ({ messages, spreadMap = null }) => {
|
||||||
|
|
||||||
const tpl = template(
|
const tpl = template(
|
||||||
[`@${rootAuthorName}`],
|
[`@${rootAuthorName}`],
|
||||||
|
hiddenRoot ? p({ class: 'muted hidden-note' }, i18n.hiddenByYou) : null,
|
||||||
div(thread(messages, spreadMap))
|
div(thread(messages, spreadMap))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||||
const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText");
|
const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText");
|
||||||
|
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon } = require("./main_views");
|
||||||
const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view");
|
const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer");
|
const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer");
|
||||||
|
|
@ -37,8 +37,9 @@ const renderTags = (tags) => {
|
||||||
let areaCounter = 0;
|
let areaCounter = 0;
|
||||||
const lastOf = (v) => (Array.isArray(v) ? v[v.length - 1] : v);
|
const lastOf = (v) => (Array.isArray(v) ? v[v.length - 1] : v);
|
||||||
const numOrNull = (v) => { const n = parseFloat(lastOf(v)); return isFinite(n) ? n : null; };
|
const numOrNull = (v) => { const n = parseFloat(lastOf(v)); return isFinite(n) ? n : null; };
|
||||||
const isBlob = (v) => !!v && String(v).startsWith("&");
|
const isSealed = (v) => !!v && String(v).startsWith("/blob/sealed/");
|
||||||
const blobSrc = (image, base) => (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : "");
|
const isBlob = (v) => !!v && (String(v).startsWith("&") || isSealed(v));
|
||||||
|
const blobSrc = (image, base) => (isSealed(image) ? (base ? "" : String(image)) : (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : ""));
|
||||||
|
|
||||||
const buildQuery = (params) => Object.entries(params || {})
|
const buildQuery = (params) => Object.entries(params || {})
|
||||||
.filter(([, v]) => v !== undefined && v !== null && String(v) !== "")
|
.filter(([, v]) => v !== undefined && v !== null && String(v) !== "")
|
||||||
|
|
@ -174,7 +175,6 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => {
|
||||||
const renderFilters = (filter, q, emptyMod = false, chip = null, search = null) =>
|
const renderFilters = (filter, q, emptyMod = false, chip = null, search = null) =>
|
||||||
div({ class: search ? "filters activity-filter-chips activity-toolbar-row" : "filters" },
|
div({ class: search ? "filters activity-filter-chips activity-toolbar-row" : "filters" },
|
||||||
form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()),
|
||||||
...(!chip || chip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase())] : []),
|
...(!chip || chip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase())] : []),
|
||||||
|
|
@ -362,7 +362,7 @@ const renderPinPopup = (mk, i, ctx = {}) => {
|
||||||
(ctx.centerHref ? `<a class="map-center-link" href="${escapeHtml(ctx.centerHref)}">${escapeHtml(i18n.mapCenterHere)}</a>` : "") +
|
(ctx.centerHref ? `<a class="map-center-link" href="${escapeHtml(ctx.centerHref)}">${escapeHtml(i18n.mapCenterHere)}</a>` : "") +
|
||||||
(own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
(own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
||||||
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
||||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, "✕")).outerHTML : "") + `</div>`
|
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))).outerHTML : "") + `</div>`
|
||||||
];
|
];
|
||||||
return parts.join("");
|
return parts.join("");
|
||||||
};
|
};
|
||||||
|
|
@ -393,7 +393,7 @@ const renderMarkersList = (markers, mapObj, ctx = {}) => {
|
||||||
centerHref ? a({ href: centerHref, class: "map-center-link" }, i18n.mapCenterHere) : null,
|
centerHref ? a({ href: centerHref, class: "map-center-link" }, i18n.mapCenterHere) : null,
|
||||||
own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
||||||
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
||||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, "✕")) : null));
|
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))) : null));
|
||||||
})));
|
})));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -531,7 +531,6 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }),
|
input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }),
|
||||||
input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }),
|
input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }),
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const detailHref = (type, key) => {
|
||||||
case 'file': return `/files/${encodeURIComponent(key)}`;
|
case 'file': return `/files/${encodeURIComponent(key)}`;
|
||||||
case 'document': return `/documents/${encodeURIComponent(key)}`;
|
case 'document': return `/documents/${encodeURIComponent(key)}`;
|
||||||
case 'feed': return `/feed/${encodeURIComponent(key)}`;
|
case 'feed': return `/feed/${encodeURIComponent(key)}`;
|
||||||
|
case 'post': return `/blogs/${encodeURIComponent(key)}`;
|
||||||
case 'votes': return `/votes/${encodeURIComponent(key)}`;
|
case 'votes': return `/votes/${encodeURIComponent(key)}`;
|
||||||
case 'transfer': return `/transfers/${encodeURIComponent(key)}`;
|
case 'transfer': return `/transfers/${encodeURIComponent(key)}`;
|
||||||
case 'industry': return `/industry/${encodeURIComponent(key)}`;
|
case 'industry': return `/industry/${encodeURIComponent(key)}`;
|
||||||
|
|
@ -264,6 +265,16 @@ const renderContentHtml = (content, key) => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'post':
|
||||||
|
return div({ class: 'opinion-blog' },
|
||||||
|
div({ class: 'card-section blog' },
|
||||||
|
content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(key)}` }, content.contentWarning))) : "",
|
||||||
|
div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text || '').trim().length > 420 ? String(content.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(content.text || '').trim())) }),
|
||||||
|
Array.isArray(content.tags) && content.tags.length
|
||||||
|
? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
);
|
||||||
case 'feed':
|
case 'feed':
|
||||||
return div({ class: 'opinion-feed' },
|
return div({ class: 'opinion-feed' },
|
||||||
div({ class: 'card-section feed' },
|
div({ class: 'card-section feed' },
|
||||||
|
|
@ -392,12 +403,12 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems =
|
||||||
{ class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') },
|
{ class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') },
|
||||||
div({ class: 'card-header activity-card-header' },
|
div({ class: 'card-header activity-card-header' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase())
|
span({ class: 'pm-exposition-text' }, String(c.type === 'post' ? (i18n.typePost || 'BLOG') : (c.type || '')).toUpperCase())
|
||||||
),
|
),
|
||||||
renderContentActions(key, detailHref(c.type, key), {
|
renderContentActions(key, detailHref(c.type, key), {
|
||||||
spread: spreadMap.get(key) || null,
|
spread: spreadMap.get(key) || null,
|
||||||
author: item.value.author,
|
author: item.value.author,
|
||||||
reportTitle: c.title || c.question || c.concept || c.name || '',
|
reportTitle: c.title || c.question || c.concept || c.name || c.contentWarning || '',
|
||||||
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
||||||
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
||||||
: {}),
|
: {}),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false }) => {
|
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false, filter = 'status', advanced = false, health = null, lan = null }) => {
|
||||||
const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe");
|
const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n, renderStateChip } = require('./main_views');
|
const { template, i18n, renderStateChip, renderModuleStats } = require('./main_views');
|
||||||
const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health');
|
const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health');
|
||||||
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
||||||
const isOutdated = (v) => {
|
const isOutdated = (v) => {
|
||||||
|
|
@ -10,6 +10,8 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
};
|
};
|
||||||
const stale = new Set(staleKeys || []);
|
const stale = new Set(staleKeys || []);
|
||||||
const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k));
|
const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k));
|
||||||
|
const knowsVersion = (keys) => keys.some(k => (versions || {})[k] || stale.has(k));
|
||||||
|
const unspecifiedHost = (h) => /^(0\.0\.0\.0|::|\[::\])$/.test(String(h || ''));
|
||||||
const versionCell = (k) => {
|
const versionCell = (k) => {
|
||||||
const v = (versions || {})[k];
|
const v = (versions || {})[k];
|
||||||
if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'];
|
if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'];
|
||||||
|
|
@ -56,7 +58,7 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
};
|
};
|
||||||
const onion = new Set(onionKeys || []);
|
const onion = new Set(onionKeys || []);
|
||||||
const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key));
|
const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key));
|
||||||
const renderPeerRow = (peerData) => {
|
const renderPeerRow = (peerData, showVersion) => {
|
||||||
const peer = peerData[1];
|
const peer = peerData[1];
|
||||||
const { name, users, key } = peer;
|
const { name, users, key } = peer;
|
||||||
const tor = overTor(peerData);
|
const tor = overTor(peerData);
|
||||||
|
|
@ -64,9 +66,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
const filteredUsers = (users || []).filter(u => u.id !== key);
|
const filteredUsers = (users || []).filter(u => u.id !== key);
|
||||||
const userCount = Array.isArray(users) ? filteredUsers.length : null;
|
const userCount = Array.isArray(users) ? filteredUsers.length : null;
|
||||||
return tr(
|
return tr(
|
||||||
td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
td({ 'data-label': i18n.peerHost || 'Pub' }, unspecifiedHost(name) ? '—' : a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
||||||
td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)),
|
td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)),
|
||||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)),
|
showVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)) : null,
|
||||||
td({ 'data-label': i18n.peersTorColumn }, tor
|
td({ 'data-label': i18n.peersTorColumn }, tor
|
||||||
? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓')
|
? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓')
|
||||||
: span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')),
|
: span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')),
|
||||||
|
|
@ -84,18 +86,20 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
|
|
||||||
const renderPeerTable = (peers, emptyKey) => {
|
const renderPeerTable = (peers, emptyKey) => {
|
||||||
if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections);
|
if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections);
|
||||||
|
const showVersion = knowsVersion(peers.map(pd => pd[1].key));
|
||||||
return table({ class: 'block-info-table' },
|
return table({ class: 'block-info-table' },
|
||||||
tr(
|
tr(
|
||||||
td({ class: 'card-label' }, i18n.peerHost || 'Pub'),
|
td({ class: 'card-label' }, i18n.peerHost || 'Pub'),
|
||||||
td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'),
|
td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'),
|
||||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
showVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||||
td({ class: 'card-label' }, i18n.peersTorColumn),
|
td({ class: 'card-label' }, i18n.peersTorColumn),
|
||||||
td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated')
|
td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated')
|
||||||
),
|
),
|
||||||
...peers.map(renderPeerRow)
|
...peers.map(pd => renderPeerRow(pd, showVersion))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const techVersion = knowsVersion((technicalPeers || []).map(tp => tp.key));
|
||||||
const technicalRows = (technicalPeers || []).map(tp => {
|
const technicalRows = (technicalPeers || []).map(tp => {
|
||||||
const k = tp.key || '';
|
const k = tp.key || '';
|
||||||
const connected = tp.state === 'connected';
|
const connected = tp.state === 'connected';
|
||||||
|
|
@ -105,9 +109,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')),
|
td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')),
|
||||||
onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))
|
onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))
|
||||||
? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||||
: td({ 'data-label': i18n.peerAddressLabel, title: String(tp.host || ''), class: 'peer-host' }, tp.host ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
: td({ 'data-label': i18n.peerAddressLabel, title: unspecifiedHost(tp.host) ? '' : String(tp.host || ''), class: 'peer-host' }, tp.host && !unspecifiedHost(tp.host) ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
||||||
td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')),
|
td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')),
|
||||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)),
|
techVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)) : null,
|
||||||
td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)),
|
td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)),
|
||||||
td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')),
|
td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')),
|
||||||
td(
|
td(
|
||||||
|
|
@ -120,31 +124,44 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh'));
|
const withCount = (label, n) => Number(n) > 0 ? `${label} (${n})` : label;
|
||||||
const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle'));
|
const peersToolbar = (tabName, total, segments) => {
|
||||||
const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'));
|
if (!(Number(total) > 0)) return null;
|
||||||
|
const back = input({ type: "hidden", name: "returnTo", value: `/peers?filter=${tabName}` });
|
||||||
|
return div({ class: 'filters activity-filter-chips activity-toolbar-row' },
|
||||||
|
renderModuleStats(total, segments),
|
||||||
|
div({ class: "conn-actions peers-conn-actions filter-box" },
|
||||||
|
form({ action: "/peers/refresh", method: "post" }, back, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh')),
|
||||||
|
form({ action: "/peers/prune", method: "post" }, back, tabName === 'lan' ? input({ type: "hidden", name: "scope", value: "lan" }) : null, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle')),
|
||||||
|
form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
const importForm = form(
|
const importForm = form(
|
||||||
{ action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" },
|
{ action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" },
|
||||||
label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'),
|
label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'),
|
||||||
br(),
|
br(),
|
||||||
textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }),
|
textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }),
|
||||||
br(),
|
|
||||||
input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }),
|
input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }),
|
||||||
br(),
|
br(),
|
||||||
button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import')
|
button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import')
|
||||||
);
|
);
|
||||||
|
|
||||||
const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' },
|
const peersTechnicalBlock = div({ class: 'peers-technical-block' },
|
||||||
h2(i18n.peerConnectionsTitle || 'Connections'),
|
peersToolbar('connections', technicalPeers.length, [
|
||||||
|
{ label: i18n.peerStateConnected, count: technicalPeers.filter(tp => tp.state === 'connected').length },
|
||||||
|
{ label: i18n.peerOutdated, count: technicalPeers.filter(tp => outdatedKey(tp.key)).length },
|
||||||
|
{ label: 'TOR', count: technicalPeers.filter(tp => onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))).length }
|
||||||
|
]),
|
||||||
|
div({ class: "tags-header" }, h2(withCount(i18n.peerConnectionsTitle || 'Connections', technicalPeers.length)), p(i18n.peersConnectionsHint)),
|
||||||
div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)),
|
div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)),
|
||||||
div({ class: "conn-actions peers-conn-actions filters" }, refreshButton, pruneButton, exportButton),
|
|
||||||
technicalPeers.length
|
technicalPeers.length
|
||||||
? table({ class: 'block-info-table' },
|
? table({ class: 'block-info-table' },
|
||||||
tr(
|
tr(
|
||||||
td({ class: 'card-label' }, i18n.peersOasisId),
|
td({ class: 'card-label' }, i18n.peersOasisId),
|
||||||
td({ class: 'card-label' }, i18n.peerAddressLabel),
|
td({ class: 'card-label' }, i18n.peerAddressLabel),
|
||||||
td({ class: 'card-label peer-port' }, i18n.peerPort),
|
td({ class: 'card-label peer-port' }, i18n.peerPort),
|
||||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
techVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||||
td({ class: 'card-label' }, i18n.peerLastChangeLabel),
|
td({ class: 'card-label' }, i18n.peerLastChangeLabel),
|
||||||
td({ class: 'card-label' }, '')
|
td({ class: 'card-label' }, '')
|
||||||
|
|
@ -155,6 +172,121 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
importForm
|
importForm
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fmtDate = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—';
|
||||||
|
const healthRows = health && Array.isArray(health.rows) ? health.rows : [];
|
||||||
|
const healthKind = (r) => {
|
||||||
|
if (r.state === 'connected') return 'connected';
|
||||||
|
if (r.dead) return 'unreachable';
|
||||||
|
if ((Number(r.failures) || 0) > 0 || r.reason) return 'failing';
|
||||||
|
return 'reachable';
|
||||||
|
};
|
||||||
|
const healthChip = (r) => {
|
||||||
|
const k = healthKind(r);
|
||||||
|
if (k === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase());
|
||||||
|
if (k === 'unreachable') return renderStateChip('closed', null, String(i18n.graphosFedUnreachable).toUpperCase());
|
||||||
|
if (k === 'failing') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[r.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase());
|
||||||
|
return renderStateChip(outdatedKey(r.key) ? 'whole' : 'mutuals', null, String(i18n.peersHealthReachable).toUpperCase());
|
||||||
|
};
|
||||||
|
const healthCount = (k) => healthRows.filter(r => healthKind(r) === k).length;
|
||||||
|
const deadCount = healthRows.filter(r => r.dead).length;
|
||||||
|
const healthVersion = knowsVersion(healthRows.map(r => r.key));
|
||||||
|
const healthBlock = !healthRows.length ? div({ class: 'peers-list' }, p(i18n.noConnections)) : div({ class: 'peers-technical-block peers-health-block' },
|
||||||
|
peersToolbar('health', healthRows.length, [
|
||||||
|
{ label: i18n.peersHealthReachable, count: healthCount('connected') + healthCount('reachable') },
|
||||||
|
{ label: i18n.peersHealthFailing, count: healthCount('failing') },
|
||||||
|
{ label: i18n.graphosFedUnreachable, count: deadCount },
|
||||||
|
{ label: i18n.peerOutdated, count: healthRows.filter(r => outdatedKey(r.key)).length },
|
||||||
|
{ label: 'TOR', count: healthRows.filter(r => r.tor).length }
|
||||||
|
]),
|
||||||
|
div({ class: "tags-header" }, h2(withCount(i18n.peersTabHealth, healthRows.length)), p(i18n.peersHealthHint)),
|
||||||
|
deadCount
|
||||||
|
? div({ class: "conn-actions peers-conn-actions filters" },
|
||||||
|
form({ action: "/peers/clear-unreachable", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peersHealthForget)))
|
||||||
|
: null,
|
||||||
|
healthRows.length
|
||||||
|
? table({ class: 'block-info-table' },
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, i18n.peerHost || 'IP'),
|
||||||
|
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||||
|
td({ class: 'card-label' }, i18n.graphosFedTransport),
|
||||||
|
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||||
|
td({ class: 'card-label' }, i18n.peersHealthFailures),
|
||||||
|
td({ class: 'card-label' }, i18n.peersFailingSince),
|
||||||
|
healthVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null
|
||||||
|
),
|
||||||
|
...healthRows.map(r => tr(
|
||||||
|
r.tor
|
||||||
|
? td({ 'data-label': i18n.peerHost || 'IP', class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||||
|
: td({ 'data-label': i18n.peerHost || 'IP', title: unspecifiedHost(r.host) ? '' : String(r.host || ''), class: 'peer-host' }, r.host && !unspecifiedHost(r.host) ? shortHost(r.host) : '—'),
|
||||||
|
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||||
|
td({ 'data-label': i18n.graphosFedTransport }, r.tor ? renderStateChip('fediverse', null, 'TOR') : renderStateChip('transport', null, String(i18n.graphosFedClearnet).toUpperCase())),
|
||||||
|
td({ 'data-label': i18n.peerStateLabel }, healthChip(r)),
|
||||||
|
td({ 'data-label': i18n.peersHealthFailures }, String(Number(r.failures) || 0)),
|
||||||
|
td({ 'data-label': i18n.peersFailingSince }, r.failingSince ? fmtDate(r.failingSince) : '—'),
|
||||||
|
healthVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null
|
||||||
|
))
|
||||||
|
)
|
||||||
|
: p(i18n.noConnections)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lanRows = lan && Array.isArray(lan.rows) ? lan.rows : [];
|
||||||
|
const lanOn = !!(lan && lan.broadcasting);
|
||||||
|
const lanVersion = knowsVersion(lanRows.map(r => r.key));
|
||||||
|
const lanBlock = div({ class: 'peers-technical-block peers-lan-block' },
|
||||||
|
peersToolbar('lan', lanRows.length, [
|
||||||
|
{ label: i18n.online, count: lanRows.filter(r => r.online).length },
|
||||||
|
{ label: i18n.peerOutdated, count: lanRows.filter(r => outdatedKey(r.key)).length }
|
||||||
|
]),
|
||||||
|
div({ class: "tags-header" }, h2(withCount(i18n.peersTabLan, lanRows.length)), p(i18n.peersLanHint)),
|
||||||
|
div({ class: "peers-pause-row" },
|
||||||
|
p({ class: "peers-pause-hint" }, `${i18n.settingsLanTitle}: `, renderStateChip(lanOn ? 'mutuals' : 'hidden', null, String(lanOn ? i18n.modulesEnabledModulesLabel : i18n.modulesDisabledModulesLabel).toUpperCase()))
|
||||||
|
),
|
||||||
|
lanRows.length
|
||||||
|
? table({ class: 'block-info-table' },
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||||
|
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||||
|
td({ class: 'card-label' }, i18n.peersLanLastSeen),
|
||||||
|
lanVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||||
|
td({ class: 'card-label' }, '')
|
||||||
|
),
|
||||||
|
...lanRows.map(r => tr(
|
||||||
|
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||||
|
td({ 'data-label': i18n.peerStateLabel }, r.online ? renderStateChip('mutuals', null, String(i18n.online).toUpperCase()) : renderStateChip('hidden', null, String(i18n.offline).toUpperCase())),
|
||||||
|
td({ 'data-label': i18n.peersLanLastSeen }, fmtDate(r.seenAt)),
|
||||||
|
lanVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null,
|
||||||
|
td(r.online ? null : form({ method: "POST", action: "/peers/lan/forget", class: "inline-form" },
|
||||||
|
input({ type: "hidden", name: "key", value: r.key }),
|
||||||
|
button({ type: "submit", class: "filter-btn" }, i18n.delete)
|
||||||
|
))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
: p(i18n.peersLanEmpty)
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = ['status', 'connections'].concat(advanced ? ['lan', 'health'] : []);
|
||||||
|
const tab = tabs.includes(filter) ? filter : 'status';
|
||||||
|
const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth };
|
||||||
|
const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [
|
||||||
|
{ label: i18n.online, count: onlineCount },
|
||||||
|
{ label: i18n.discovered, count: discoveredCount },
|
||||||
|
{ label: i18n.unknown, count: unknownCount }
|
||||||
|
]);
|
||||||
|
const statusBlock = (onlineCount + discoveredCount + unknownCount) > 0
|
||||||
|
? div({ class: "peers-list" },
|
||||||
|
statusToolbar,
|
||||||
|
...[
|
||||||
|
[i18n.online, onlineCount, dedupOnline, 'noConnections', i18n.peersOnlineHint],
|
||||||
|
[i18n.discovered, discoveredCount, dedupDiscovered, 'noDiscovered', i18n.peersDiscoveredHint],
|
||||||
|
[i18n.unknown, unknownCount, dedupUnknown, 'noUnknownPeers', i18n.peersUnknownHint]
|
||||||
|
].filter(([, count]) => count > 0).flatMap(([title, count, list, emptyKey, hint], idx) => [
|
||||||
|
idx ? hr() : null,
|
||||||
|
div({ class: "tags-header" }, h2(withCount(title, count)), p(hint)),
|
||||||
|
renderPeerTable(list, emptyKey)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: div({ class: "peers-list" }, statusToolbar, p(i18n.noConnections));
|
||||||
|
|
||||||
return template(
|
return template(
|
||||||
i18n.peers,
|
i18n.peers,
|
||||||
section(
|
section(
|
||||||
|
|
@ -162,20 +294,14 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
||||||
h2(i18n.peers),
|
h2(i18n.peers),
|
||||||
p(i18n.peerConnectionsIntro)
|
p(i18n.peerConnectionsIntro)
|
||||||
),
|
),
|
||||||
(onlineCount + discoveredCount + unknownCount) > 0
|
div({ class: 'mode-buttons-row' },
|
||||||
? div({ class: "peers-list" },
|
...tabs.map(t => a({ class: tab === t ? 'filter-btn active' : 'filter-btn', href: `/peers?filter=${t}` }, String(tabLabel[t]).toUpperCase()))
|
||||||
div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)),
|
),
|
||||||
renderPeerTable(dedupOnline, 'noConnections'),
|
tab === 'status' ? statusBlock : null,
|
||||||
hr(),
|
tab === 'connections' ? peersTechnicalBlock : null,
|
||||||
div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)),
|
tab === 'connections' ? p(i18n.connectionActionIntro) : null,
|
||||||
renderPeerTable(dedupDiscovered, 'noDiscovered'),
|
tab === 'lan' ? lanBlock : null,
|
||||||
hr(),
|
tab === 'health' ? healthBlock : null
|
||||||
div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)),
|
|
||||||
renderPeerTable(dedupUnknown, 'noUnknownPeers')
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
peersTechnicalBlock,
|
|
||||||
p(i18n.connectionActionIntro)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
const { div, h2, p, section, button, form, input, span, audio, table, tr, td, th, br, a } = require("../server/node_modules/hyperaxe");
|
const { div, h2, p, section, button, form, input, span, audio, table, tr, td, th, br, a } = require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid } = require("./main_views");
|
const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid, renderIcon } = require("./main_views");
|
||||||
const { renderEncryptedChip } = require("./clearnet_view");
|
const { renderEncryptedChip } = require("./clearnet_view");
|
||||||
const { phoneNumberOf } = require("../models/phone_number");
|
const { phoneNumberOf } = require("../models/phone_number");
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
|
|
||||||
const PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS;
|
const PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS;
|
||||||
|
const GROUP_MAX = require('../server/phone_module').GROUP_MAX;
|
||||||
|
|
||||||
const clock = (ms) => {
|
const clock = (ms) => {
|
||||||
const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
||||||
|
|
@ -105,7 +106,7 @@ const NUMBER_TOKEN = /^\d{3}-\d{3}$/;
|
||||||
|
|
||||||
const renderCompose = (compose) => {
|
const renderCompose = (compose) => {
|
||||||
const recipients = Array.isArray(compose.recipients) ? compose.recipients : [];
|
const recipients = Array.isArray(compose.recipients) ? compose.recipients : [];
|
||||||
const max = Number(compose.max) || 7;
|
const max = Number(compose.max) || GROUP_MAX;
|
||||||
const dial = String(compose.dial || '');
|
const dial = String(compose.dial || '');
|
||||||
const full = recipients.length >= max;
|
const full = recipients.length >= max;
|
||||||
const keep = () => [input({ type: 'hidden', name: 'filter', value: 'create' }), input({ type: 'hidden', name: 'to', value: compose.to || '' })];
|
const keep = () => [input({ type: 'hidden', name: 'filter', value: 'create' }), input({ type: 'hidden', name: 'to', value: compose.to || '' })];
|
||||||
|
|
@ -127,7 +128,7 @@ const renderCompose = (compose) => {
|
||||||
key('clear', '✕', 'phone-key phone-key-fn', { title: i18n.phoneDialClear })
|
key('clear', '✕', 'phone-key phone-key-fn', { title: i18n.phoneDialClear })
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
p({ class: 'phone-empty phone-dial-hint' }, i18n.phoneLimitsHint),
|
p({ class: 'phone-empty phone-dial-hint' }, String(i18n.phoneLimitsHint).replace('{n}', String(max))),
|
||||||
div({ class: 'phone-recipients' },
|
div({ class: 'phone-recipients' },
|
||||||
span({ class: 'card-label' }, `${i18n.phoneDialInhabitants}: ${recipients.length}/${max}`),
|
span({ class: 'card-label' }, `${i18n.phoneDialInhabitants}: ${recipients.length}/${max}`),
|
||||||
recipients.length ? div({ class: 'phone-recipient-list' },
|
recipients.length ? div({ class: 'phone-recipient-list' },
|
||||||
|
|
@ -183,7 +184,7 @@ const renderRecords = (pams) =>
|
||||||
const deleteCalls = (ids) => ids.length
|
const deleteCalls = (ids) => ids.length
|
||||||
? form({ method: 'POST', action: '/phone/delete-shown', class: 'content-action-form phone-row-delete' },
|
? form({ method: 'POST', action: '/phone/delete-shown', class: 'content-action-form phone-row-delete' },
|
||||||
...ids.map(id => input({ type: 'hidden', name: 'calls', value: id })),
|
...ids.map(id => input({ type: 'hidden', name: 'calls', value: id })),
|
||||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, '✕')
|
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, renderIcon('close'))
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
||||||
section(
|
section(
|
||||||
div({ class: "filters" },
|
div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/podcasts", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/podcasts", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||||
...chipButtons(CATEGORIES, filter, census, catLabel)
|
...chipButtons(CATEGORIES, filter, census, catLabel)
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ exports.singlePollView = async (poll, comments = [], params = {}) => {
|
||||||
: (poll.voters.length
|
: (poll.voters.length
|
||||||
? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v)))
|
? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v)))
|
||||||
: null),
|
: null),
|
||||||
renderEngagement(poll.id,
|
poll.tribeId ? null : renderEngagement(poll.id,
|
||||||
renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants),
|
renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants),
|
||||||
renderComments(poll, comments, basePath)
|
renderComments(poll, comments, basePath)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe")
|
const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe")
|
||||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed } = require("./main_views")
|
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon } = require("./main_views")
|
||||||
const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view")
|
const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view")
|
||||||
const { renderStyledText } = require("../backend/renderStyledText")
|
const { renderStyledText } = require("../backend/renderStyledText")
|
||||||
const moment = require("../server/node_modules/moment")
|
const moment = require("../server/node_modules/moment")
|
||||||
|
|
@ -68,7 +68,7 @@ const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null
|
const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: String(room.image).startsWith("/blob/sealed/") ? room.image : `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null
|
||||||
|
|
||||||
const roomIsFull = (occ) => !!occ && occ.count >= ((occ && occ.max) || ROOM_MAX)
|
const roomIsFull = (occ) => !!occ && occ.count >= ((occ && occ.max) || ROOM_MAX)
|
||||||
|
|
||||||
|
|
@ -194,18 +194,6 @@ const renderRecChip = () => span({ class: "room-rec-chip" }, "● " + String(i18
|
||||||
const fmtDuration = (ms) => { const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}` }
|
const fmtDuration = (ms) => { const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}` }
|
||||||
const refreshLink = (room) => a({ href: roomHref(room), class: "tribe-action-btn room-refresh-btn" }, String(i18n.liveRefresh).toUpperCase())
|
const refreshLink = (room) => a({ href: roomHref(room), class: "tribe-action-btn room-refresh-btn" }, String(i18n.liveRefresh).toUpperCase())
|
||||||
const renderLiveControls = (room, live) => div({ class: "room-live-controls" },
|
const renderLiveControls = (room, live) => div({ class: "room-live-controls" },
|
||||||
form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" },
|
|
||||||
input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }),
|
|
||||||
button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase())
|
|
||||||
),
|
|
||||||
form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" },
|
|
||||||
input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }),
|
|
||||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
|
||||||
button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase())
|
|
||||||
),
|
|
||||||
live.notify && safeArr(live.events).length
|
|
||||||
? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase()))
|
|
||||||
: null,
|
|
||||||
form({ method: "POST", action: "/rooms/mute", class: "phone-action-form" },
|
form({ method: "POST", action: "/rooms/mute", class: "phone-action-form" },
|
||||||
input({ type: "hidden", name: "mute", value: live.muted ? "0" : "1" }),
|
input({ type: "hidden", name: "mute", value: live.muted ? "0" : "1" }),
|
||||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||||
|
|
@ -214,14 +202,40 @@ const renderLiveControls = (room, live) => div({ class: "room-live-controls" },
|
||||||
live.videoAvailable
|
live.videoAvailable
|
||||||
? form({ method: "POST", action: "/rooms/camera", class: "phone-action-form" },
|
? form({ method: "POST", action: "/rooms/camera", class: "phone-action-form" },
|
||||||
input({ type: "hidden", name: "cam", value: live.cam ? "0" : "1" }),
|
input({ type: "hidden", name: "cam", value: live.cam ? "0" : "1" }),
|
||||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }),
|
||||||
button({ type: "submit", class: live.cam ? "tribe-action-btn room-cam-btn-on" : "tribe-action-btn" }, String(live.cam ? i18n.phoneCameraOff : i18n.phoneCameraOn).toUpperCase())
|
button({ type: "submit", class: live.cam ? "tribe-action-btn room-cam-btn-on" : "tribe-action-btn" }, String(live.cam ? i18n.phoneCameraOff : i18n.phoneCameraOn).toUpperCase())
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
live.streaming != null
|
||||||
|
? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" },
|
||||||
|
input({ type: "hidden", name: "stop", value: "1" }),
|
||||||
|
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||||
|
button({ type: "submit", class: "tribe-action-btn room-cam-btn-on" }, String(i18n.roomStreamStop).toUpperCase())
|
||||||
|
)
|
||||||
|
: live.streamAvailable
|
||||||
|
? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form room-stream-form" },
|
||||||
|
input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }),
|
||||||
|
input({ type: "file", id: "room-stream-file", name: "video", accept: "video/*", required: true, class: "room-stream-input" }),
|
||||||
|
label({ for: "room-stream-file", class: "tribe-action-btn room-stream-pick" }, String(i18n.roomStreamFile).toUpperCase()),
|
||||||
|
button({ type: "submit", class: "tribe-action-btn room-cam-btn-on room-stream-go", title: i18n.roomStreamGo }, "▶")
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" },
|
||||||
|
input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }),
|
||||||
|
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||||
|
button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase())
|
||||||
|
),
|
||||||
live.recording
|
live.recording
|
||||||
? form({ method: "POST", action: "/rooms/rec/stop", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomRecStop).toUpperCase()))
|
? form({ method: "POST", action: "/rooms/rec/stop", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomRecStop).toUpperCase()))
|
||||||
: form({ method: "POST", action: "/rooms/rec/start", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomRec).toUpperCase())),
|
: form({ method: "POST", action: "/rooms/rec/start", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomRec).toUpperCase())),
|
||||||
live.recording || safeArr(live.recordingBy).length ? renderRecChip() : null,
|
live.recording || safeArr(live.recordingBy).length ? renderRecChip() : null,
|
||||||
|
form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" },
|
||||||
|
input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }),
|
||||||
|
button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase())
|
||||||
|
),
|
||||||
|
live.notify && safeArr(live.events).length
|
||||||
|
? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase()))
|
||||||
|
: null,
|
||||||
refreshLink(room),
|
refreshLink(room),
|
||||||
form({ method: "POST", action: "/rooms/leave", class: "phone-action-form" },
|
form({ method: "POST", action: "/rooms/leave", class: "phone-action-form" },
|
||||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||||
|
|
@ -245,7 +259,7 @@ const renderRecordings = (recordings) => safeArr(recordings).length
|
||||||
span({ class: "phone-call-clock" }, fmtDuration(r.durationMs)),
|
span({ class: "phone-call-clock" }, fmtDuration(r.durationMs)),
|
||||||
a({ href: `/rooms/recordings/${encodeURIComponent(r.name)}`, class: "tribe-action-btn", download: r.name }, String(i18n.fileDownload).toUpperCase()),
|
a({ href: `/rooms/recordings/${encodeURIComponent(r.name)}`, class: "tribe-action-btn", download: r.name }, String(i18n.fileDownload).toUpperCase()),
|
||||||
form({ method: "POST", action: `/rooms/recordings/${encodeURIComponent(r.name)}/delete`, class: "phone-action-form" },
|
form({ method: "POST", action: `/rooms/recordings/${encodeURIComponent(r.name)}/delete`, class: "phone-action-form" },
|
||||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, "✕"))
|
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, renderIcon("close")))
|
||||||
)))
|
)))
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
@ -260,34 +274,44 @@ const renderLivePanel = (room, live, occ, canJoin = false) => {
|
||||||
input({ type: "hidden", name: "on", value: on ? "0" : "1" }),
|
input({ type: "hidden", name: "on", value: on ? "0" : "1" }),
|
||||||
button({ type: "submit", class: on ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(on ? i18n.phoneUnsilence : i18n.phoneSilence).toUpperCase())
|
button({ type: "submit", class: on ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(on ? i18n.phoneUnsilence : i18n.phoneSilence).toUpperCase())
|
||||||
)
|
)
|
||||||
const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false) => div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") },
|
const isPublic = !!live.public || (room.clearnet === true && clearnetEligible(room))
|
||||||
|
const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false, film = false, viewer = false) => viewer
|
||||||
|
? div({ class: "phone-contact room-peer" }, userLink(id), renderReachChip(true, i18n))
|
||||||
|
: div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") },
|
||||||
turn ? span({ class: "room-peer-hand" }, `✋ ${turn}`) : null,
|
turn ? span({ class: "room-peer-hand" }, `✋ ${turn}`) : null,
|
||||||
userLink(id),
|
userLink(id),
|
||||||
you ? span({ class: "room-peer-you" }, i18n.roomYou) : null,
|
you ? span({ class: "room-peer-you" }, i18n.roomYou) : null,
|
||||||
muted ? span({ class: "room-peer-muted" }, String(i18n.roomMuted).toUpperCase()) : null,
|
muted ? span({ class: "room-peer-muted" }, String(i18n.roomMuted).toUpperCase()) : null,
|
||||||
cam ? span({ class: "room-peer-cam" }, "CAM") : null,
|
cam ? span({ class: "room-peer-cam" }, "CAM") : null,
|
||||||
|
film ? span({ class: "room-peer-cam" }, "▶") : null,
|
||||||
recording ? renderRecChip() : null,
|
recording ? renderRecChip() : null,
|
||||||
you ? null : silenceForm(id, silenced)
|
you ? null : silenceForm(id, silenced)
|
||||||
)
|
)
|
||||||
return div({ class: "phone-call-panel room-live-panel" },
|
return div({ class: "phone-call-panel room-live-panel" },
|
||||||
head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null), renderLiveControls(room, live)),
|
head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "▶ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)),
|
||||||
live.grid
|
live.grid
|
||||||
? (safeArr(live.gridOrder).length
|
? (safeArr(live.gridOrder).length
|
||||||
? div({ class: "video-grid video-grid-one" },
|
? div({ id: "room-video", class: safeArr(live.gridOrder).length === 1 ? "video-grid video-grid-one video-grid-solo" : "video-grid video-grid-one" },
|
||||||
...renderLiveFeed("/phone/video/grid", "video-feed video-feed-grid"),
|
...renderLiveFeed("/phone/video/grid", "video-feed video-feed-grid"),
|
||||||
div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(id => id === userId ? span({ class: "video-tile-name" }, i18n.roomYou) : userLink(id)))
|
div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(key => {
|
||||||
|
const film = String(key).startsWith("film:")
|
||||||
|
const id = film ? String(key).slice(5) : key
|
||||||
|
const name = id === userId ? i18n.roomYou : userLink(id)
|
||||||
|
return film ? span({ class: "video-tile-name video-tile-film" }, "▶ ", name) : (id === userId ? span({ class: "video-tile-name" }, name) : name)
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
: null)
|
: null)
|
||||||
: renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam })), !!live.cam),
|
: div({ id: "room-video" }, renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam, film: !!p.film })), !!live.cam, { film: live.streaming != null })),
|
||||||
div({ class: "phone-contacts" }, ...handOrder(live).map(e => e.you
|
div({ class: "phone-contacts" }, ...handOrder(live).map(e => e.you
|
||||||
? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam)
|
? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam, live.streaming != null)
|
||||||
: who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam))),
|
: who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam, !!e.peer.film, !!e.peer.viewer))),
|
||||||
renderRoomEvents(live)
|
renderRoomEvents(live)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const count = occ ? occ.count : 0
|
const count = occ ? occ.count : 0
|
||||||
return div({ class: "phone-block room-live-panel" },
|
return div({ class: "phone-block room-live-panel" },
|
||||||
head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null)
|
head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null),
|
||||||
|
canJoin && room.clearnet === true && clearnetEligible(room) ? p({ class: "room-public-warning" }, i18n.roomPublicWarning) : null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,9 +351,13 @@ exports.singleRoomView = async (room, params = {}) => {
|
||||||
|
|
||||||
const ownerActions = isAuthor
|
const ownerActions = isAuthor
|
||||||
? [
|
? [
|
||||||
room.isClosed ? null : form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` },
|
room.isClosed
|
||||||
button({ type: "submit", class: "update-btn" }, i18n.roomClose)
|
? form({ method: "POST", action: `/rooms/reopen/${encodeURIComponent(room.rootId)}` },
|
||||||
),
|
button({ type: "submit", class: "update-btn" }, i18n.roomReopen)
|
||||||
|
)
|
||||||
|
: form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` },
|
||||||
|
button({ type: "submit", class: "update-btn delete-btn" }, i18n.roomClose)
|
||||||
|
),
|
||||||
form({ method: "GET", action: "/rooms" },
|
form({ method: "GET", action: "/rooms" },
|
||||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||||
input({ type: "hidden", name: "id", value: room.rootId }),
|
input({ type: "hidden", name: "id", value: room.rootId }),
|
||||||
|
|
@ -423,8 +451,8 @@ exports.renderTribeRoomsSection = (tribe, rooms, occupancy, live, toolbar = null
|
||||||
exports.renderRoomItem = renderRoomItem
|
exports.renderRoomItem = renderRoomItem
|
||||||
exports.roomStatsBy = (rooms) => renderModuleStatsBy(safeArr(rooms), r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }])
|
exports.roomStatsBy = (rooms) => renderModuleStatsBy(safeArr(rooms), r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }])
|
||||||
|
|
||||||
exports.clearnetRoomView = async (room) => {
|
exports.clearnetRoomView = async (room, { live = false, sound = true, base = "" } = {}) => {
|
||||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage } = require("./clearnet_view")
|
const { escapeHtml: esc, renderKindTag, renderClearnetPage, cnZoom } = require("./clearnet_view")
|
||||||
const name = room.title || i18n.cnKindRoom
|
const name = room.title || i18n.cnKindRoom
|
||||||
const extraCss = `
|
const extraCss = `
|
||||||
.cn-room-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word}
|
.cn-room-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word}
|
||||||
|
|
@ -432,6 +460,13 @@ exports.clearnetRoomView = async (room) => {
|
||||||
.cn-room-number{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--fg);border-radius:10px;padding:4px 12px;font-size:16px;font-weight:600;color:var(--fg);background:var(--bg-sub)}
|
.cn-room-number{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--fg);border-radius:10px;padding:4px 12px;font-size:16px;font-weight:600;color:var(--fg);background:var(--bg-sub)}
|
||||||
.cn-room-number .pm-exposition-icon{font-size:18px;line-height:1}
|
.cn-room-number .pm-exposition-icon{font-size:18px;line-height:1}
|
||||||
.cn-room-number .pm-exposition-text{line-height:1;letter-spacing:1px;font-family:monospace}
|
.cn-room-number .pm-exposition-text{line-height:1;letter-spacing:1px;font-family:monospace}
|
||||||
|
.cn-room-live{display:flex;flex-direction:column;align-items:center;gap:12px;margin-top:24px}
|
||||||
|
.cn-room-live-chip{align-self:flex-start;border:1px solid #2ecc71;border-radius:10px;padding:3px 10px;color:#2ecc71;font-size:13px;font-weight:700;letter-spacing:1px}
|
||||||
|
.cn-room-live .cn-zoom{display:block;width:100%;max-width:960px}
|
||||||
|
.cn-room-grid{width:100%;aspect-ratio:4/3;object-fit:contain;background:#000;border-radius:8px}
|
||||||
|
.cn-room-sound{width:100%;max-width:960px}
|
||||||
|
.cn-room-live .cn-lightbox-img{width:92vw;height:88vh;object-fit:contain}
|
||||||
|
.cn-room-live .cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh}
|
||||||
`
|
`
|
||||||
const body = `
|
const body = `
|
||||||
<h1 class="cn-room-title">${esc(name)}</h1>
|
<h1 class="cn-room-title">${esc(name)}</h1>
|
||||||
|
|
@ -439,6 +474,11 @@ exports.clearnetRoomView = async (room) => {
|
||||||
${renderKindTag("room")}
|
${renderKindTag("room")}
|
||||||
${room.number ? `<span class="pm-exposition-chip pm-exposition-whole cn-room-number"><span class="pm-exposition-icon">✆</span><span class="pm-exposition-text">${esc(room.number)}</span></span>` : ""}
|
${room.number ? `<span class="pm-exposition-chip pm-exposition-whole cn-room-number"><span class="pm-exposition-icon">✆</span><span class="pm-exposition-text">${esc(room.number)}</span></span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
${live && base ? `<section class="cn-room-live">
|
||||||
|
<span class="cn-room-live-chip">● ${esc(String(i18n.roomLiveChip || "LIVE").toUpperCase())}</span>
|
||||||
|
${cnZoom("room-live", `${base}/live`, name, { full: true, imgClass: "cn-room-grid" })}
|
||||||
|
${sound ? `<audio class="cn-room-sound" controls preload="none" src="${esc(`${base}/sound`)}"></audio>` : ""}
|
||||||
|
</section>` : ""}
|
||||||
`
|
`
|
||||||
return renderClearnetPage({
|
return renderClearnetPage({
|
||||||
title: `${name} | Oasis`,
|
title: `${name} | Oasis`,
|
||||||
|
|
@ -461,3 +501,26 @@ exports.roomModesFromCensus = (census, me, occupancy) => {
|
||||||
closed: list.some(x => x.isClosed)
|
closed: list.some(x => x.isClosed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickerSize = (bytes) => {
|
||||||
|
const n = Number(bytes) || 0
|
||||||
|
if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB`
|
||||||
|
if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB`
|
||||||
|
return `${Math.max(1, Math.round(n / 1024))} KB`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.roomStreamMatchesView = ({ files, returnTo }) => template(
|
||||||
|
i18n.roomStreamPickTitle,
|
||||||
|
section(
|
||||||
|
div({ class: "tags-header module-header-line" }, h2(i18n.roomStreamPickTitle), p(i18n.roomStreamPickHint)),
|
||||||
|
div({ class: "mode-buttons-row" }, a({ href: returnTo, class: "filter-btn" }, String(i18n.phoneCancel).toUpperCase())),
|
||||||
|
ul({ class: "stream-picker-list" }, ...safeArr(files).map(f => li({ class: "stream-picker-item" },
|
||||||
|
span({ class: "stream-picker-link" }, renderIcon("film"), span({ class: "stream-picker-name" }, f.path), span({ class: "stream-picker-size" }, pickerSize(f.size))),
|
||||||
|
form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" },
|
||||||
|
input({ type: "hidden", name: "file", value: f.path }),
|
||||||
|
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||||
|
button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomStreamFile).toUpperCase())
|
||||||
|
)
|
||||||
|
)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe");
|
const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe");
|
||||||
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||||
const { template, i18n, userLink, renderContentActions, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views');
|
const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views');
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText');
|
const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText');
|
||||||
const { renderStyledText } = require('../backend/renderStyledText');
|
const { renderStyledText } = require('../backend/renderStyledText');
|
||||||
|
|
@ -18,16 +18,51 @@ const industryStatusLabel = (status) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => {
|
const renderSearchForm = ({ action, query = "", queryName = "query", hidden = [] } = {}) => {
|
||||||
const searchInput = input({
|
const searchInput = input({ id: "search_query", name: queryName, required: false, type: "search", value: query, placeholder: i18n.searchPlaceholder });
|
||||||
id: "search_query",
|
|
||||||
name: "query",
|
|
||||||
required: false,
|
|
||||||
type: "search",
|
|
||||||
value: query,
|
|
||||||
placeholder: i18n.searchPlaceholder
|
|
||||||
});
|
|
||||||
searchInput.setAttribute("minlength", 3);
|
searchInput.setAttribute("minlength", 3);
|
||||||
|
const resultCount = currentPerPage();
|
||||||
|
const resultsPerPageSelect = select(
|
||||||
|
{ id: "results-per-page", name: "perPage", class: "input-select search-select" },
|
||||||
|
LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s)))
|
||||||
|
);
|
||||||
|
return form(
|
||||||
|
{ action, method: "GET", class: "search-form" },
|
||||||
|
...hidden.map(([name, value]) => input({ type: "hidden", name, value })),
|
||||||
|
div({ class: "search-filters-row" },
|
||||||
|
table({ class: "search-filters-table" },
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")),
|
||||||
|
td(input({ id: "search_from", type: "datetime-local", name: "from" }))
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")),
|
||||||
|
td(input({ id: "search_to", type: "datetime-local", name: "to" }))
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")),
|
||||||
|
td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" }))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
table({ class: "search-filters-table" },
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")),
|
||||||
|
td(searchInput)
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")),
|
||||||
|
td(resultsPerPageSelect)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
div({ class: "search-submit-row" },
|
||||||
|
button({ type: "submit" }, i18n.searchSubmit)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
exports.renderSearchForm = renderSearchForm;
|
||||||
|
|
||||||
|
const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => {
|
||||||
|
|
||||||
const contentTypes = [
|
const contentTypes = [
|
||||||
"post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes",
|
"post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes",
|
||||||
|
|
@ -49,15 +84,6 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const resultCount = currentPerPage();
|
|
||||||
const resultsPerPageSelect = select(
|
|
||||||
{
|
|
||||||
id: "results-per-page",
|
|
||||||
name: "perPage",
|
|
||||||
class: "input-select search-select"
|
|
||||||
},
|
|
||||||
LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s)))
|
|
||||||
);
|
|
||||||
|
|
||||||
const getViewDetailsActionForSearch = (type, contentId, content) => {
|
const getViewDetailsActionForSearch = (type, contentId, content) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -75,7 +101,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`;
|
case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`;
|
||||||
case 'event': return `/events/${encodeURIComponent(contentId)}`;
|
case 'event': return `/events/${encodeURIComponent(contentId)}`;
|
||||||
case 'task': return `/tasks/${encodeURIComponent(contentId)}`;
|
case 'task': return `/tasks/${encodeURIComponent(contentId)}`;
|
||||||
case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`;
|
case 'post': return content && typeof content.root === 'string'
|
||||||
|
? `/thread/${encodeURIComponent(content.root)}#${encodeURIComponent(contentId)}`
|
||||||
|
: `/blogs/${encodeURIComponent(contentId)}`;
|
||||||
case 'market': return `/market/${encodeURIComponent(contentId)}`;
|
case 'market': return `/market/${encodeURIComponent(contentId)}`;
|
||||||
case 'report': return `/reports/${encodeURIComponent(contentId)}`;
|
case 'report': return `/reports/${encodeURIComponent(contentId)}`;
|
||||||
case 'project': return `/projects/${encodeURIComponent(contentId)}`;
|
case 'project': return `/projects/${encodeURIComponent(contentId)}`;
|
||||||
|
|
@ -124,8 +152,11 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
case 'post':
|
case 'post':
|
||||||
return div({ class: 'search-post' },
|
return div({ class: 'search-post' },
|
||||||
content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null,
|
content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, content.contentWarning)) : null,
|
||||||
content.text ? div({ class: 'card-field' }, span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text)) })) : null
|
content.text ? div({ class: 'blog-card-text post-text-clamped', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text).trim().length > 1000 ? String(content.text).trim().slice(0, 1000).replace(/\s+$/, '') + '…' : String(content.text).trim())) }) : null,
|
||||||
|
Array.isArray(content.tags) && content.tags.length
|
||||||
|
? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
case 'about':
|
case 'about':
|
||||||
return div({ class: 'card-section about' },
|
return div({ class: 'card-section about' },
|
||||||
|
|
@ -596,6 +627,12 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeLabel = (type) => String(i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || type || '').toUpperCase();
|
||||||
|
const totalResults = Object.values(results).reduce((n, msgs) => n + (Array.isArray(msgs) ? msgs.length : 0), 0);
|
||||||
|
const resultStats = renderModuleStats(totalResults,
|
||||||
|
Object.entries(results)
|
||||||
|
.map(([type, msgs]) => ({ label: typeLabel(type), count: Array.isArray(msgs) ? msgs.length : 0 }))
|
||||||
|
.sort((x, y) => y.count - x.count || x.label.localeCompare(y.label)));
|
||||||
const pageGroups = paged(Object.entries(results).flatMap(([key, msgs]) => msgs.map((msg) => [key, msg])))
|
const pageGroups = paged(Object.entries(results).flatMap(([key, msgs]) => msgs.map((msg) => [key, msg])))
|
||||||
.reduce((acc, [key, msg]) => { (acc[key] = acc[key] || []).push(msg); return acc; }, {});
|
.reduce((acc, [key, msg]) => { (acc[key] = acc[key] || []).push(msg); return acc; }, {});
|
||||||
const resultSection = Object.entries(results).length > 0
|
const resultSection = Object.entries(results).length > 0
|
||||||
|
|
@ -642,7 +679,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
div({ class: 'card-header activity-card-header' },
|
div({ class: 'card-header activity-card-header' },
|
||||||
div({ class: 'card-chips-row' },
|
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(content.type || '').toUpperCase())
|
span({ class: 'pm-exposition-text' }, String(i18n['type' + String(content.type || '').charAt(0).toUpperCase() + String(content.type || '').slice(1)] || content.type || '').toUpperCase())
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), {
|
renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), {
|
||||||
|
|
@ -676,46 +713,18 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
let html = template(
|
let html = template(
|
||||||
hashtag ? `#${hashtag}` : i18n.searchTitle,
|
i18n.searchTitle,
|
||||||
section(
|
section(
|
||||||
div({ class: "tags-header module-header-line" },
|
div({ class: "tags-header module-header-line" },
|
||||||
h2(hashtag ? `#${hashtag}` : i18n.searchTitle),
|
h2(i18n.searchTitle),
|
||||||
p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)
|
p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)
|
||||||
),
|
),
|
||||||
form(
|
renderSearchForm({ action: "/search", query })
|
||||||
{ action: "/search", method: "GET", class: "search-form" },
|
|
||||||
div({ class: "search-filters-row" },
|
|
||||||
table({ class: "search-filters-table" },
|
|
||||||
tr(
|
|
||||||
td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")),
|
|
||||||
td(input({ id: "search_from", type: "datetime-local", name: "from" }))
|
|
||||||
),
|
|
||||||
tr(
|
|
||||||
td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")),
|
|
||||||
td(input({ id: "search_to", type: "datetime-local", name: "to" }))
|
|
||||||
),
|
|
||||||
tr(
|
|
||||||
td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")),
|
|
||||||
td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" }))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
table({ class: "search-filters-table" },
|
|
||||||
tr(
|
|
||||||
td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")),
|
|
||||||
td(searchInput)
|
|
||||||
),
|
|
||||||
tr(
|
|
||||||
td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")),
|
|
||||||
td(resultsPerPageSelect)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
div({ class: "search-submit-row" },
|
|
||||||
button({ type: "submit" }, i18n.searchSubmit)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
section(resultSection)
|
section(
|
||||||
|
resultStats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, resultStats) : null,
|
||||||
|
resultSection
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasDocument) {
|
if (hasDocument) {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ const { form, button, div, h2, h3, p, section, select, option, input, br, a, lab
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getConfig } = require('../configs/config-manager.js');
|
const { getConfig } = require('../configs/config-manager.js');
|
||||||
const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES } = require('./main_views');
|
const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, userLink } = require('./main_views');
|
||||||
const i18nBase = require("../client/assets/translations/i18n");
|
const i18nBase = require("../client/assets/translations/i18n");
|
||||||
|
const moment = require("../server/node_modules/moment");
|
||||||
const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model');
|
const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model');
|
||||||
const html = require("../server/node_modules/hyperaxe");
|
const html = require("../server/node_modules/hyperaxe");
|
||||||
// selector de identidades: la implementacion vive en ./identities_view.js
|
// selector de identidades: la implementacion vive en ./identities_view.js
|
||||||
|
|
@ -12,6 +13,8 @@ const { renderVerificationReport, renderRebuildReport } = require('./backup_view
|
||||||
|
|
||||||
const snhUrl = "https://wiki.solarnethub.com/socialnet/overview";
|
const snhUrl = "https://wiki.solarnethub.com/socialnet/overview";
|
||||||
const BLOB_CACHE_OPTIONS = [512, 1024, 2048, 5120, 10240, 0];
|
const BLOB_CACHE_OPTIONS = [512, 1024, 2048, 5120, 10240, 0];
|
||||||
|
const plainLabel = (s) => String(s || '').replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim();
|
||||||
|
const hiddenAt = (at) => at ? moment(at).format("YYYY/MM/DD HH:mm") : "—";
|
||||||
const sizeLabel = (bytes) => {
|
const sizeLabel = (bytes) => {
|
||||||
let value = Number(bytes) || 0;
|
let value = Number(bytes) || 0;
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
|
@ -44,7 +47,9 @@ const settingsView = (params) => {
|
||||||
const walletUser = currentConfig.wallet.user;
|
const walletUser = currentConfig.wallet.user;
|
||||||
const walletFee = currentConfig.wallet.fee;
|
const walletFee = currentConfig.wallet.fee;
|
||||||
const currentWish = ['mutuals', 'only-lan', 'local'].includes(currentConfig.wish) ? currentConfig.wish : 'whole';
|
const currentWish = ['mutuals', 'only-lan', 'local'].includes(currentConfig.wish) ? currentConfig.wish : 'whole';
|
||||||
const currentPmVisibility = currentConfig.pmVisibility === 'mutuals' ? 'mutuals' : 'whole';
|
const currentPmVisibility = ['mutuals', 'following'].includes(currentConfig.pmVisibility) ? currentConfig.pmVisibility : 'whole';
|
||||||
|
const currentMentionsVisibility = ['mutuals', 'following'].includes(currentConfig.mentionsVisibility) ? currentConfig.mentionsVisibility : 'whole';
|
||||||
|
const viewerHidden = (() => { try { const vf = require('../models/viewer_filters'); return { muted: vf.mutedList(), hidden: vf.hiddenList() }; } catch (_) { return { muted: [], hidden: [] }; } })();
|
||||||
const currentPhone = currentConfig.phone && typeof currentConfig.phone === 'object' ? currentConfig.phone : {};
|
const currentPhone = currentConfig.phone && typeof currentConfig.phone === 'object' ? currentConfig.phone : {};
|
||||||
|
|
||||||
const themeElements = THEMES.map(([value, name]) => option({ value, ...(theme === value ? { selected: true } : {}) }, name));
|
const themeElements = THEMES.map(([value, name]) => option({ value, ...(theme === value ? { selected: true } : {}) }, name));
|
||||||
|
|
@ -115,7 +120,7 @@ const settingsView = (params) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : 'oasis';
|
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis';
|
||||||
const oasisParts = [
|
const oasisParts = [
|
||||||
updatePanel,
|
updatePanel,
|
||||||
renderIdentities(require('../server/ssb_config').account),
|
renderIdentities(require('../server/ssb_config').account),
|
||||||
|
|
@ -156,7 +161,8 @@ const settingsView = (params) => {
|
||||||
const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on';
|
const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on';
|
||||||
const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on';
|
const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on';
|
||||||
const phoneEnabled = !!(currentConfig.modules && currentConfig.modules.phoneMod !== 'off');
|
const phoneEnabled = !!(currentConfig.modules && currentConfig.modules.phoneMod !== 'off');
|
||||||
const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : "blocks";
|
const roomsEnabled = !!(currentConfig.modules && currentConfig.modules.roomsMod !== 'off');
|
||||||
|
const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks";
|
||||||
const uxCard = (value, title, image) => label({ class: "welcome-ux-option" },
|
const uxCard = (value, title, image) => label({ class: "welcome-ux-option" },
|
||||||
input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }),
|
input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }),
|
||||||
img({ src: image, class: "welcome-ux-shot", alt: title }),
|
img({ src: image, class: "welcome-ux-shot", alt: title }),
|
||||||
|
|
@ -167,7 +173,8 @@ const settingsView = (params) => {
|
||||||
aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null,
|
aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null,
|
||||||
chatsEnabled ? uxCard("chats", i18n.uxModeChats || "Conversations", "/assets/images/ux-chats.png") : null,
|
chatsEnabled ? uxCard("chats", i18n.uxModeChats || "Conversations", "/assets/images/ux-chats.png") : null,
|
||||||
uxCard("feed", i18n.uxModeFeed || "Microblogging", "/assets/images/ux-feed.png"),
|
uxCard("feed", i18n.uxModeFeed || "Microblogging", "/assets/images/ux-feed.png"),
|
||||||
phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null
|
phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null,
|
||||||
|
roomsEnabled ? uxCard("rooms", i18n.roomsTitle || "Rooms", "/assets/images/ux-rooms.png") : null
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
button({ type: "submit" }, i18n.saveSettings)
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
|
@ -187,22 +194,6 @@ const settingsView = (params) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
section({ id: "wish" },
|
|
||||||
div({ class: "tags-header" },
|
|
||||||
h2(i18n.settingsWishTitle),
|
|
||||||
p(i18n.settingsWishDesc),
|
|
||||||
form(
|
|
||||||
{ action: "/settings/wish", method: "POST" },
|
|
||||||
select({ name: "wish" },
|
|
||||||
option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole),
|
|
||||||
option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals),
|
|
||||||
option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"),
|
|
||||||
option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local")
|
|
||||||
), br(), br(),
|
|
||||||
button({ type: "submit" }, i18n.saveSettings)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
workflowSection,
|
workflowSection,
|
||||||
section({ id: "home-page" },
|
section({ id: "home-page" },
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
|
|
@ -282,48 +273,10 @@ const settingsView = (params) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
blobCache ? section({ id: "blobcache" },
|
|
||||||
div({ class: "tags-header" },
|
|
||||||
h2(i18n.settingsBlobCacheTitle),
|
|
||||||
p(i18n.settingsBlobCacheDesc),
|
|
||||||
blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null,
|
|
||||||
blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null,
|
|
||||||
form(
|
|
||||||
{ action: "/settings/blob-cache", method: "POST" },
|
|
||||||
label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit),
|
|
||||||
br(),
|
|
||||||
select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))),
|
|
||||||
br(), br(),
|
|
||||||
button({ type: "submit" }, i18n.saveSettings),
|
|
||||||
blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : null,
|
|
||||||
section({ id: "replication" },
|
|
||||||
div({ class: "tags-header" },
|
|
||||||
h2(i18n.settingsReplicationTitle || 'Replication'),
|
|
||||||
p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'),
|
|
||||||
form(
|
|
||||||
{ action: "/settings/replication", method: "POST" },
|
|
||||||
label({ for: "replication_hops" }, i18n.settingsReplicationHopsLabel || 'Hops'),
|
|
||||||
br(),
|
|
||||||
input({
|
|
||||||
type: "number",
|
|
||||||
id: "replication_hops",
|
|
||||||
name: "hops",
|
|
||||||
min: 0,
|
|
||||||
max: 6,
|
|
||||||
value: currentHops
|
|
||||||
}),
|
|
||||||
br(), br(),
|
|
||||||
button({ type: "submit" }, i18n.saveSettings)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
section({ id: "lan" },
|
section({ id: "lan" },
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
h2(i18n.settingsLanTitle || 'LAN Broadcasting'),
|
h2(i18n.settingsLanTitle || 'LAN Broadcasting'),
|
||||||
p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.'),
|
p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network.'),
|
||||||
form(
|
form(
|
||||||
{ action: "/settings/lan-broadcasting", method: "POST" },
|
{ action: "/settings/lan-broadcasting", method: "POST" },
|
||||||
label({ for: "lanBroadcasting", class: "lan-checkbox-label" },
|
label({ for: "lanBroadcasting", class: "lan-checkbox-label" },
|
||||||
|
|
@ -342,20 +295,6 @@ const settingsView = (params) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
section({ id: "pm-visibility" },
|
|
||||||
div({ class: "tags-header" },
|
|
||||||
h2(i18n.settingsPmVisibilityTitle),
|
|
||||||
p(i18n.settingsPmVisibilityDesc),
|
|
||||||
form(
|
|
||||||
{ action: "/settings/pm-visibility", method: "POST" },
|
|
||||||
select({ name: "pmVisibility" },
|
|
||||||
option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
|
||||||
option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
|
||||||
), br(), br(),
|
|
||||||
button({ type: "submit" }, i18n.saveSettings)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
modOn('phone') ? section({ id: "phone" },
|
modOn('phone') ? section({ id: "phone" },
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
h2(i18n.phoneTitle),
|
h2(i18n.phoneTitle),
|
||||||
|
|
@ -376,30 +315,6 @@ const settingsView = (params) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : null,
|
) : null,
|
||||||
modOn('inbox') ? section({ id: "inbox-bots" },
|
|
||||||
div({ class: "tags-header" },
|
|
||||||
h2(i18n.settingsInboxBotsTitle),
|
|
||||||
p(i18n.settingsInboxBotsDesc),
|
|
||||||
form(
|
|
||||||
{ action: "/settings/inbox-bots", method: "POST" },
|
|
||||||
...INBOX_BOT_ORDER.map(bot =>
|
|
||||||
label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" },
|
|
||||||
input({
|
|
||||||
type: "checkbox",
|
|
||||||
id: `inbox-bot-${bot}`,
|
|
||||||
name: "bots",
|
|
||||||
value: bot,
|
|
||||||
class: "lan-checkbox-input",
|
|
||||||
checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true
|
|
||||||
}),
|
|
||||||
span({ class: "lan-checkbox-text" }, inboxBotLabel(bot))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
br(),
|
|
||||||
button({ type: "submit" }, i18n.saveSettings)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : null,
|
|
||||||
modOn('wallet') ? section(
|
modOn('wallet') ? section(
|
||||||
{ id: "wallet" },
|
{ id: "wallet" },
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
|
|
@ -461,6 +376,156 @@ const settingsView = (params) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
const contentParts = [
|
||||||
|
section({ id: "wish" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsWishTitle),
|
||||||
|
p(i18n.settingsWishDesc),
|
||||||
|
form(
|
||||||
|
{ action: "/settings/wish", method: "POST" },
|
||||||
|
select({ name: "wish" },
|
||||||
|
option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole),
|
||||||
|
option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals),
|
||||||
|
option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"),
|
||||||
|
option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local")
|
||||||
|
), br(), br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
section({ id: "replication" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsReplicationTitle || 'Replication'),
|
||||||
|
p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'),
|
||||||
|
form(
|
||||||
|
{ action: "/settings/replication", method: "POST" },
|
||||||
|
label({ for: "replication_hops" }, i18n.settingsReplicationHopsLabel || 'Hops'),
|
||||||
|
br(),
|
||||||
|
input({
|
||||||
|
type: "number",
|
||||||
|
id: "replication_hops",
|
||||||
|
name: "hops",
|
||||||
|
min: 0,
|
||||||
|
max: 6,
|
||||||
|
value: currentHops
|
||||||
|
}),
|
||||||
|
br(), br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
blobCache ? section({ id: "blobcache" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsBlobCacheTitle),
|
||||||
|
p(i18n.settingsBlobCacheDesc),
|
||||||
|
blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null,
|
||||||
|
blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null,
|
||||||
|
form(
|
||||||
|
{ action: "/settings/blob-cache", method: "POST" },
|
||||||
|
label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit),
|
||||||
|
br(),
|
||||||
|
select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))),
|
||||||
|
br(), br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings),
|
||||||
|
blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : null,
|
||||||
|
section({ id: "pm-visibility" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsPmVisibilityTitle),
|
||||||
|
p(i18n.settingsPmVisibilityDesc),
|
||||||
|
form(
|
||||||
|
{ action: "/settings/pm-visibility", method: "POST" },
|
||||||
|
select({ name: "pmVisibility" },
|
||||||
|
option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||||
|
option({ value: "following", ...(currentPmVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing),
|
||||||
|
option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||||
|
), br(), br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
section({ id: "mentions-visibility" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsMentionsVisibilityTitle),
|
||||||
|
p(i18n.settingsMentionsVisibilityDesc),
|
||||||
|
form(
|
||||||
|
{ action: "/settings/mentions-visibility", method: "POST" },
|
||||||
|
select({ name: "mentionsVisibility" },
|
||||||
|
option({ value: "whole", ...(currentMentionsVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||||
|
option({ value: "following", ...(currentMentionsVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing),
|
||||||
|
option({ value: "mutuals", ...(currentMentionsVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||||
|
), br(), br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modOn('inbox') ? section({ id: "inbox-bots" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsInboxBotsTitle),
|
||||||
|
p(i18n.settingsInboxBotsDesc),
|
||||||
|
form(
|
||||||
|
{ action: "/settings/inbox-bots", method: "POST" },
|
||||||
|
...INBOX_BOT_ORDER.map(bot =>
|
||||||
|
label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" },
|
||||||
|
input({
|
||||||
|
type: "checkbox",
|
||||||
|
id: `inbox-bot-${bot}`,
|
||||||
|
name: "bots",
|
||||||
|
value: bot,
|
||||||
|
class: "lan-checkbox-input",
|
||||||
|
checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true
|
||||||
|
}),
|
||||||
|
span({ class: "lan-checkbox-text" }, inboxBotLabel(bot))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
br(),
|
||||||
|
button({ type: "submit" }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : null,
|
||||||
|
section({ id: "hidden" },
|
||||||
|
div({ class: "tags-header" },
|
||||||
|
h2(i18n.settingsHiddenTitle),
|
||||||
|
p(i18n.settingsHiddenDesc),
|
||||||
|
p(strong(`${i18n.settingsMutedLabel} (${viewerHidden.muted.length})`)),
|
||||||
|
viewerHidden.muted.length
|
||||||
|
? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" },
|
||||||
|
tr(
|
||||||
|
td({ class: "card-label" }, i18n.inhabitantProfileTitle),
|
||||||
|
td({ class: "card-label" }, i18n.date),
|
||||||
|
td({ class: "card-label" }, "")
|
||||||
|
),
|
||||||
|
...viewerHidden.muted.map(m => tr(
|
||||||
|
td({ "data-label": i18n.inhabitantProfileTitle }, userLink(m.id)),
|
||||||
|
td({ "data-label": i18n.date }, hiddenAt(m.at)),
|
||||||
|
td({ "data-label": "" }, form({ action: `/unmute/${encodeURIComponent(m.id)}`, method: "POST" },
|
||||||
|
input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }),
|
||||||
|
button({ type: "submit", class: "filter-btn" }, i18n.unmute)))
|
||||||
|
))
|
||||||
|
))
|
||||||
|
: p({ class: "muted" }, i18n.settingsMutedEmpty),
|
||||||
|
p(strong(`${i18n.settingsHiddenLabel} (${viewerHidden.hidden.length})`)),
|
||||||
|
viewerHidden.hidden.length
|
||||||
|
? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" },
|
||||||
|
tr(
|
||||||
|
td({ class: "card-label" }, i18n.settingsTabContent),
|
||||||
|
td({ class: "card-label" }, i18n.date),
|
||||||
|
td({ class: "card-label" }, "")
|
||||||
|
),
|
||||||
|
...viewerHidden.hidden.map(h => tr(
|
||||||
|
td({ "data-label": i18n.settingsTabContent }, h.href ? a({ href: h.href }, plainLabel(h.label) || h.id) : span(plainLabel(h.label) || h.id)),
|
||||||
|
td({ "data-label": i18n.date }, hiddenAt(h.at)),
|
||||||
|
td({ "data-label": "" }, form({ action: `/unhide/${encodeURIComponent(h.id)}`, method: "POST" },
|
||||||
|
input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }),
|
||||||
|
button({ type: "submit", class: "filter-btn" }, i18n.unhideContent)))
|
||||||
|
))
|
||||||
|
))
|
||||||
|
: p({ class: "muted" }, i18n.settingsHiddenEmpty)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
const multiverseParts = modOn('fediverse') ? [
|
const multiverseParts = modOn('fediverse') ? [
|
||||||
section({ id: "mastodon" },
|
section({ id: "mastodon" },
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
|
|
@ -586,10 +651,11 @@ const settingsView = (params) => {
|
||||||
),
|
),
|
||||||
div({ class: "mode-buttons-row" },
|
div({ class: "mode-buttons-row" },
|
||||||
a({ class: filter === 'oasis' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=oasis' }, String(i18n.linkBoxOasis).toUpperCase()),
|
a({ class: filter === 'oasis' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=oasis' }, String(i18n.linkBoxOasis).toUpperCase()),
|
||||||
|
a({ class: filter === 'content' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=content' }, String(i18n.settingsTabContent).toUpperCase()),
|
||||||
modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null
|
modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
...(filter === 'multiverse' ? multiverseParts : oasisParts)
|
...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : oasisParts)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,11 @@ const renderTorrentSeeds = (t, spreadMap) => {
|
||||||
const info = spreadInfoOf(t, spreadMap);
|
const info = spreadInfoOf(t, spreadMap);
|
||||||
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
||||||
};
|
};
|
||||||
const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||||
|
|
||||||
const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, params = {}) => {
|
const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, params = {}) => {
|
||||||
const returnTo = buildReturnTo(filter, params);
|
const returnTo = buildReturnTo(filter, params);
|
||||||
|
const extraColumns = typeof params.rowFor === "function";
|
||||||
|
|
||||||
if (!torrents.length) return p(params.q ? i18n.torrentNoMatch : i18n.noTorrents);
|
if (!torrents.length) return p(params.q ? i18n.torrentNoMatch : i18n.noTorrents);
|
||||||
|
|
||||||
|
|
@ -91,21 +92,18 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param
|
||||||
{ border: "1", class: "torrent-table" },
|
{ border: "1", class: "torrent-table" },
|
||||||
tr(
|
tr(
|
||||||
th(i18n.createdAt || "DATE"),
|
th(i18n.createdAt || "DATE"),
|
||||||
th(i18n.authorLabel || "AUTHOR"),
|
|
||||||
th(i18n.torrentTitleLabel || "TITLE"),
|
th(i18n.torrentTitleLabel || "TITLE"),
|
||||||
th(i18n.torrentSizeLabel || "SIZE"),
|
th(i18n.torrentSizeLabel || "SIZE"),
|
||||||
th(i18n.seedsLabel),
|
th({ class: "torrent-spread-cell" }, i18n.seedsLabel),
|
||||||
th(i18n.seedAction),
|
th({ class: "torrent-spread-cell" }, i18n.seedAction),
|
||||||
th(""),
|
th(""),
|
||||||
th(""),
|
...(extraColumns ? [th(""), th("")] : [])
|
||||||
th("")
|
|
||||||
),
|
),
|
||||||
torrents.map((t) => {
|
torrents.map((t) => {
|
||||||
const row = typeof params.rowFor === "function" ? params.rowFor(t) : null;
|
const row = typeof params.rowFor === "function" ? params.rowFor(t) : null;
|
||||||
return tr(
|
return tr(
|
||||||
td({ class: "row-chips" }, t.tribeOrigin ? renderTribeOriginChip(t.tribeOrigin) : t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('torrents', t.title, t.key)) : null, ...(row && Array.isArray(row.titleChips) ? row.titleChips.filter(Boolean) : [])),
|
td({ class: "row-chips" }, t.tribeOrigin ? renderTribeOriginChip(t.tribeOrigin) : t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('torrents', t.title, t.key)) : null, ...(row && Array.isArray(row.titleChips) ? row.titleChips.filter(Boolean) : [])),
|
||||||
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||||
td({ class: "row-author" }, userLink(t.author)),
|
|
||||||
td({ class: "row-title", "data-label": i18n.torrentTitleLabel || "TITLE" }, t.title || ""),
|
td({ class: "row-title", "data-label": i18n.torrentTitleLabel || "TITLE" }, t.title || ""),
|
||||||
td({ "data-label": i18n.torrentSizeLabel || "SIZE" }, formatSize(t.size)),
|
td({ "data-label": i18n.torrentSizeLabel || "SIZE" }, formatSize(t.size)),
|
||||||
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, t.tribeOrigin || row ? "" : renderTorrentSeeds(t, params.spreadMap)),
|
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, t.tribeOrigin || row ? "" : renderTorrentSeeds(t, params.spreadMap)),
|
||||||
|
|
@ -124,25 +122,7 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param
|
||||||
button({ type: "submit", class: "filter-btn" }, i18n.torrentDetailsButton)
|
button({ type: "submit", class: "filter-btn" }, i18n.torrentDetailsButton)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
td(
|
...(extraColumns ? [td(row ? (row.download || "") : ""), td({ class: "row-actions" }, row ? (row.actions || "") : "")] : [])
|
||||||
row
|
|
||||||
? (row.download || "")
|
|
||||||
: t.url && t.url.startsWith("&")
|
|
||||||
? div({ class: "torrent-card-actions" }, renderTorrentSourceDownload(t.key, t.source), renderTorrentDownload(torrentDownloadHref(t.url, t.title)))
|
|
||||||
: ""
|
|
||||||
),
|
|
||||||
td({ class: "row-actions" },
|
|
||||||
row
|
|
||||||
? (row.actions || "")
|
|
||||||
: t.tribeOrigin ? "" : renderContentActions(t.key, `/torrents/${encodeURIComponent(t.key)}`, {
|
|
||||||
author: t.author,
|
|
||||||
favKind: 'torrents',
|
|
||||||
isFavorite: t.isFavorite,
|
|
||||||
returnTo,
|
|
||||||
reportTitle: t.title,
|
|
||||||
deleteAction: String(t.author) === String(userId) ? contentDeleteAction('torrent', t.key) : undefined
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -308,7 +288,6 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()),
|
||||||
|
|
@ -449,7 +428,7 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
renderEngagement(torrentObj.key,
|
torrentObj.tribeId ? null : renderEngagement(torrentObj.key,
|
||||||
renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants),
|
renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants),
|
||||||
renderTorrentCommentsSection(torrentObj.key, comments, returnTo)
|
renderTorrentCommentsSection(torrentObj.key, comments, returnTo)
|
||||||
)
|
)
|
||||||
|
|
@ -466,7 +445,6 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,6 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => {
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }),
|
input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }),
|
||||||
input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }),
|
input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }),
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
|
|
@ -558,7 +557,6 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }),
|
input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }),
|
||||||
input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }),
|
input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }),
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,15 @@ const { sanitizeHtml } = require('../backend/sanitizeHtml');
|
||||||
|
|
||||||
const userId = config.keys.id;
|
const userId = config.keys.id;
|
||||||
|
|
||||||
|
const capFirst = (s) => String(s || '').charAt(0).toUpperCase() + String(s || '').slice(1);
|
||||||
|
const typeLabel = (type) => i18n['type' + capFirst(type)] || type;
|
||||||
|
|
||||||
const filterButton = (mode, currentFilter) =>
|
const filterButton = (mode, currentFilter) =>
|
||||||
form({ method: 'GET', action: '/trending' },
|
form({ method: 'GET', action: '/trending' },
|
||||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||||
button(
|
button(
|
||||||
{ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
|
{ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
|
||||||
String(i18n[mode + 'Button'] || mode).toUpperCase()
|
String(i18n[mode + 'Button'] || typeLabel(mode)).toUpperCase()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -162,6 +165,16 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else if (c.type === 'post') {
|
||||||
|
contentHtml = div({ class: 'trending-blog' },
|
||||||
|
div({ class: 'card-section blog' },
|
||||||
|
c.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(item.key)}` }, c.contentWarning))) : "",
|
||||||
|
div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(c.text || '').trim().length > 420 ? String(c.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(c.text || '').trim())) }),
|
||||||
|
Array.isArray(c.tags) && c.tags.length
|
||||||
|
? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
);
|
||||||
} else if (c.type === 'feed') {
|
} else if (c.type === 'feed') {
|
||||||
const { text, refeeds } = c;
|
const { text, refeeds } = c;
|
||||||
contentHtml = div({ class: 'trending-feed' },
|
contentHtml = div({ class: 'trending-feed' },
|
||||||
|
|
@ -221,6 +234,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
||||||
file: 'files',
|
file: 'files',
|
||||||
document: 'documents',
|
document: 'documents',
|
||||||
feed: 'feed',
|
feed: 'feed',
|
||||||
|
post: 'blogs',
|
||||||
votes: 'votes',
|
votes: 'votes',
|
||||||
transfer: 'transfers',
|
transfer: 'transfers',
|
||||||
industry: 'industry',
|
industry: 'industry',
|
||||||
|
|
@ -249,12 +263,12 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
||||||
div(
|
div(
|
||||||
{ class: 'card-header activity-card-header' },
|
{ class: 'card-header activity-card-header' },
|
||||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||||
span({ class: 'pm-exposition-text' }, String(i18n['type' + String(c.type || '').charAt(0).toUpperCase() + String(c.type || '').slice(1)] || c.type || '').toUpperCase())
|
span({ class: 'pm-exposition-text' }, String(typeLabel(c.type) || '').toUpperCase())
|
||||||
),
|
),
|
||||||
renderContentActions(item.key, detailHref, {
|
renderContentActions(item.key, detailHref, {
|
||||||
spread: spreadMap.get(item.key) || null,
|
spread: spreadMap.get(item.key) || null,
|
||||||
author: item.value.author,
|
author: item.value.author,
|
||||||
reportTitle: c.title || c.question || c.concept || '',
|
reportTitle: c.title || c.question || c.concept || c.contentWarning || '',
|
||||||
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
||||||
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
||||||
: {}),
|
: {}),
|
||||||
|
|
@ -301,7 +315,7 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap
|
||||||
const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP'];
|
const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP'];
|
||||||
const contentFilters = [
|
const contentFilters = [
|
||||||
['votes', 'event', 'task', 'report'],
|
['votes', 'event', 'task', 'report'],
|
||||||
['feed', 'project', 'industry', 'shopProduct', 'transfer'],
|
['feed', 'post', 'project', 'industry', 'shopProduct', 'transfer'],
|
||||||
['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'],
|
['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'],
|
||||||
['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute']
|
['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute']
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const { renderCalendarCard } = require('./calendars_view');
|
||||||
const { renderInhabitantCard } = require('./inhabitants_view');
|
const { renderInhabitantCard } = require('./inhabitants_view');
|
||||||
const { config } = require('../server/SSB_server.js');
|
const { config } = require('../server/SSB_server.js');
|
||||||
const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText');
|
const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText');
|
||||||
|
const { renderSearchForm } = require('./search_view');
|
||||||
const { renderMapCard, renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
const { renderMapCard, renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||||
const { renderTribeRoomsSection, roomStatsBy } = require('./rooms_view');
|
const { renderTribeRoomsSection, roomStatsBy } = require('./rooms_view');
|
||||||
|
|
||||||
|
|
@ -110,18 +111,19 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => {
|
||||||
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||||
if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ loading: 'lazy', src: s, ...attrs })
|
if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ loading: 'lazy', src: s, ...attrs })
|
||||||
if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
||||||
const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||||
if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' })
|
if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: toBlobUrl(mVideo[1]), preload: 'metadata' })
|
||||||
const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||||
if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` })
|
if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: toBlobUrl(mAudio[1]) })
|
||||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||||
if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
if (mImg) return img({ loading: 'lazy', src: toBlobUrl(mImg[1]), ...attrs })
|
||||||
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const toBlobUrl = (raw) => {
|
const toBlobUrl = (raw) => {
|
||||||
if (!raw) return null
|
if (!raw) return null
|
||||||
const s = String(raw).trim()
|
const s = String(raw).trim()
|
||||||
|
if (s.startsWith('/blob/sealed/')) return s
|
||||||
if (s.startsWith('&')) return `/blob/${encodeURIComponent(s)}`
|
if (s.startsWith('&')) return `/blob/${encodeURIComponent(s)}`
|
||||||
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/)
|
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/)
|
||||||
return m ? `/blob/${encodeURIComponent(m[1])}` : null
|
return m ? `/blob/${encodeURIComponent(m[1])}` : null
|
||||||
|
|
@ -387,6 +389,15 @@ const sectionLink = (tribe, sectionKey, label, currentSection) =>
|
||||||
button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, String(label).toUpperCase())
|
button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, String(label).toUpperCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tribeTagHref = (tribe) => (tag) => `/tribe/${encodeURIComponent(tribe.id)}?section=search&q=${encodeURIComponent('#' + String(tag).toLowerCase().replace(/^#+/, ''))}`;
|
||||||
|
const tribeText = (tribe, text, opts = {}) => renderStyledText(String(text || ''), { ...opts, ...(tribe && tribe.id ? { hashtagHref: tribeTagHref(tribe) } : {}) });
|
||||||
|
const tribeExcerpt = (tribe, text, max = 200) => {
|
||||||
|
const full = String(text || '').trim();
|
||||||
|
if (!full) return null;
|
||||||
|
const cut = full.length > max ? full.slice(0, max).replace(/\s+$/, '') + '…' : full;
|
||||||
|
return p({ class: 'post-text post-text-pre' }, ...tribeText(tribe, cut));
|
||||||
|
};
|
||||||
|
|
||||||
const renderSectionNav = (tribe, section) => {
|
const renderSectionNav = (tribe, section) => {
|
||||||
const firstGroup = [{ key: 'activity', label: i18n.tribeSectionActivity }, { key: 'inhabitants', label: i18n.tribeSectionInhabitants }];
|
const firstGroup = [{ key: 'activity', label: i18n.tribeSectionActivity }, { key: 'inhabitants', label: i18n.tribeSectionInhabitants }];
|
||||||
if (!tribe.parentTribeId) firstGroup.push({ key: 'subtribes', label: i18n.tribeSubTribes });
|
if (!tribe.parentTribeId) firstGroup.push({ key: 'subtribes', label: i18n.tribeSubTribes });
|
||||||
|
|
@ -449,7 +460,7 @@ const contentTypeVerb = (ct) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentTypeName = (ct) => {
|
const contentTypeName = (ct) => {
|
||||||
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: i18n.pollsTitle, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: i18n.blogTitle, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' };
|
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: String(i18n.pollsTitle || 'POLLS').toUpperCase(), forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: String(i18n.blogTitle || 'BLOGS').toUpperCase(), media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' };
|
||||||
return map[ct] || ct;
|
return map[ct] || ct;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -494,7 +505,7 @@ const renderTribeActivitySection = (tribe, sectionData) => {
|
||||||
),
|
),
|
||||||
div({ class: 'tribe-card-body' },
|
div({ class: 'tribe-card-body' },
|
||||||
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
||||||
item.description ? p(String(item.description).substring(0, 200)) : null,
|
tribeExcerpt(tribe, item.description),
|
||||||
replies.map(r => div({ class: 'thread-reply-item' },
|
replies.map(r => div({ class: 'thread-reply-item' },
|
||||||
r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null,
|
r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null,
|
||||||
div({ class: 'card-footer thread-reply-footer' },
|
div({ class: 'card-footer thread-reply-footer' },
|
||||||
|
|
@ -546,7 +557,7 @@ const renderTribeActivitySection = (tribe, sectionData) => {
|
||||||
span({ class: 'card-value' }, item.title)
|
span({ class: 'card-value' }, item.title)
|
||||||
) : null,
|
) : null,
|
||||||
mediaContent,
|
mediaContent,
|
||||||
item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? p(item.description.substring(0, 200)) : null
|
item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? tribeExcerpt(tribe, item.description) : null
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
p({ class: 'card-footer' },
|
||||||
span({ class: 'date-link' }, `${date}`),
|
span({ class: 'date-link' }, `${date}`),
|
||||||
|
|
@ -589,11 +600,8 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => {
|
||||||
|
|
||||||
const renderTribeTagsSection = (tribe, sectionData, query) => {
|
const renderTribeTagsSection = (tribe, sectionData, query) => {
|
||||||
const tagsList = Array.isArray(sectionData?.tags) ? sectionData.tags : [];
|
const tagsList = Array.isArray(sectionData?.tags) ? sectionData.tags : [];
|
||||||
const selectedTag = sectionData?.selectedTag || '';
|
|
||||||
const filteredItems = Array.isArray(sectionData?.filteredItems) ? sectionData.filteredItems : [];
|
|
||||||
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
|
||||||
const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||||||
const threadsById = reachIndexOf(tagsList.flatMap(t => Array.isArray(t.items) ? t.items : []));
|
const tagHref = tribeTagHref(tribe);
|
||||||
return section(
|
return section(
|
||||||
div({ class: 'tags-header module-header-line' },
|
div({ class: 'tags-header module-header-line' },
|
||||||
h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'),
|
h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'),
|
||||||
|
|
@ -608,29 +616,13 @@ const renderTribeTagsSection = (tribe, sectionData, query) => {
|
||||||
th(i18n.tagsTableHeaderCount || 'Count')
|
th(i18n.tagsTableHeaderCount || 'Count')
|
||||||
)),
|
)),
|
||||||
tbody(
|
tbody(
|
||||||
(selectedTag ? sortedTags : paged(sortedTags)).map(t => tr(
|
paged(sortedTags).map(t => tr(
|
||||||
td(a({ href: `${tribeUrl}?section=tags&tag=${encodeURIComponent(t.tag)}` }, t.tag)),
|
td(a({ href: tagHref(t.tag) }, t.tag)),
|
||||||
td(`${t.count}`)
|
td(`${t.count}`)
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
selectedTag ? div({ class: 'tribe-content-list' },
|
|
||||||
h2(`#${selectedTag} (${filteredItems.length})`),
|
|
||||||
filteredItems.length === 0 ? p(i18n.tribeTagsEmpty || 'No items') :
|
|
||||||
paged(filteredItems).map(item => div({ class: 'card card-rpg' },
|
|
||||||
div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`)),
|
|
||||||
div({ class: 'card-body' },
|
|
||||||
renderTribeReachListChip(tribe, item, threadsById),
|
|
||||||
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
|
||||||
item.description ? p(item.description.substring(0, 200)) : null
|
|
||||||
),
|
|
||||||
p({ class: 'card-footer' },
|
|
||||||
span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
|
||||||
userLink(item.author)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
) : null
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -639,16 +631,19 @@ const renderTribeSearchSection = (tribe, sectionData, query) => {
|
||||||
const sq = sectionData?.query || '';
|
const sq = sectionData?.query || '';
|
||||||
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
||||||
const threadsById = reachIndexOf(results);
|
const threadsById = reachIndexOf(results);
|
||||||
|
const hashtag = sq.trim().startsWith('#') ? sq.trim().replace(/^#+/, '').toLowerCase() : '';
|
||||||
|
const typeCounts = new Map();
|
||||||
|
for (const item of results) {
|
||||||
|
const label = String(contentTypeName(item.contentType)).toUpperCase();
|
||||||
|
typeCounts.set(label, (typeCounts.get(label) || 0) + 1);
|
||||||
|
}
|
||||||
|
const stats = renderModuleStats(results.length, [...typeCounts].map(([label, count]) => ({ label, count })).sort((x, y) => y.count - x.count || x.label.localeCompare(y.label)));
|
||||||
return div({ class: 'tribe-content-list' },
|
return div({ class: 'tribe-content-list' },
|
||||||
div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionSearch)),
|
div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionSearch), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)),
|
||||||
form({ method: 'GET', action: tribeUrl, class: 'tribe-search-form' },
|
renderSearchForm({ action: tribeUrl, query: sq, queryName: 'q', hidden: [['section', 'search']] }),
|
||||||
input({ type: 'hidden', name: 'section', value: 'search' }),
|
|
||||||
input({ type: 'text', name: 'q', value: sq, placeholder: i18n.tribeSearchPlaceholder, minlength: 2 }),
|
|
||||||
button({ type: 'submit', class: 'create-button' }, i18n.tribeSectionSearch)
|
|
||||||
),
|
|
||||||
sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null,
|
sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null,
|
||||||
sq.length >= 2 ? div(
|
sq.length >= 2 ? div(
|
||||||
h2(`${i18n.tribeSearchResults}: ${results.length}`),
|
stats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, stats) : null,
|
||||||
results.length === 0 ? p(i18n.tribeSearchEmpty) :
|
results.length === 0 ? p(i18n.tribeSearchEmpty) :
|
||||||
paged(results).map(item => div({ class: 'card card-rpg' },
|
paged(results).map(item => div({ class: 'card card-rpg' },
|
||||||
div({ class: 'card-header' },
|
div({ class: 'card-header' },
|
||||||
|
|
@ -660,7 +655,7 @@ const renderTribeSearchSection = (tribe, sectionData, query) => {
|
||||||
span({ class: 'card-label' }, (i18n.title || 'Title') + ':'),
|
span({ class: 'card-label' }, (i18n.title || 'Title') + ':'),
|
||||||
span({ class: 'card-value' }, item.title)
|
span({ class: 'card-value' }, item.title)
|
||||||
) : null,
|
) : null,
|
||||||
item.description ? p(item.description.substring(0, 200)) : null
|
tribeExcerpt(tribe, item.description)
|
||||||
),
|
),
|
||||||
p({ class: 'card-footer' },
|
p({ class: 'card-footer' },
|
||||||
span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||||
|
|
@ -686,7 +681,7 @@ const renderOverviewSection = (tribe, query, sectionData) => {
|
||||||
recentFeed.map(m =>
|
recentFeed.map(m =>
|
||||||
div({ class: 'feed-item' },
|
div({ class: 'feed-item' },
|
||||||
p(`${moment(m.createdAt).format("YYYY/MM/DD HH:mm")} — `, userLink(m.author)),
|
p(`${moment(m.createdAt).format("YYYY/MM/DD HH:mm")} — `, userLink(m.author)),
|
||||||
p(...renderStyledText(m.description))
|
p(...tribeText(tribe, m.description))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -743,7 +738,7 @@ const tribeStatusOf = (item) => String(item.status || 'OPEN').toUpperCase();
|
||||||
const capFirst = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1);
|
const capFirst = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1);
|
||||||
const blobIdOf = (raw) => {
|
const blobIdOf = (raw) => {
|
||||||
const s = String(raw || '').trim();
|
const s = String(raw || '').trim();
|
||||||
if (s.startsWith('&')) return s;
|
if (s.startsWith('&') || s.startsWith('/blob/sealed/')) return s;
|
||||||
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/);
|
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/);
|
||||||
return m ? m[1] : null;
|
return m ? m[1] : null;
|
||||||
};
|
};
|
||||||
|
|
@ -864,6 +859,7 @@ const renderFeedTribeView = (feedItems, tribe, query = {}) => {
|
||||||
const chip = tribeReachChipFor(tribe, m);
|
const chip = tribeReachChipFor(tribe, m);
|
||||||
const controls = renderTribeReachControls(tribe, m, returnTo);
|
const controls = renderTribeReachControls(tribe, m, returnTo);
|
||||||
return renderFeedCard(toFeedShape(m), null, {
|
return renderFeedCard(toFeedShape(m), null, {
|
||||||
|
hashtagHref: tribeTagHref(tribe),
|
||||||
refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`,
|
refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`,
|
||||||
canRefeed: member,
|
canRefeed: member,
|
||||||
headerActions: tribeItemActions(tribe, m, member, m.description),
|
headerActions: tribeItemActions(tribe, m, member, m.description),
|
||||||
|
|
@ -1193,7 +1189,7 @@ const renderForumThreadDetail = (tribe, allItems, threadId, member) => {
|
||||||
reachChip
|
reachChip
|
||||||
),
|
),
|
||||||
renderTribeReachControls(tribe, thread, returnTo),
|
renderTribeReachControls(tribe, thread, returnTo),
|
||||||
div({ class: 'forum-body' }, ...renderStyledText(thread.description || '')),
|
div({ class: 'forum-body' }, ...tribeText(tribe, thread.description || '')),
|
||||||
div({ class: 'forum-meta' },
|
div({ class: 'forum-meta' },
|
||||||
span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`),
|
span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`),
|
||||||
span({ class: 'forum-participants' }, `${String(i18n.forumParticipants).toUpperCase()}: ${participants.size}`),
|
span({ class: 'forum-participants' }, `${String(i18n.forumParticipants).toUpperCase()}: ${participants.size}`),
|
||||||
|
|
@ -1227,7 +1223,7 @@ const renderForumThreadDetail = (tribe, allItems, threadId, member) => {
|
||||||
div({ class: 'comment-body-row' },
|
div({ class: 'comment-body-row' },
|
||||||
div({ class: 'comment-vote-col' }, forumScoreBox(tribe, r, member, threadId)),
|
div({ class: 'comment-vote-col' }, forumScoreBox(tribe, r, member, threadId)),
|
||||||
div({ class: 'comment-text-col' },
|
div({ class: 'comment-text-col' },
|
||||||
div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...renderStyledText(l, { zoomImages: true }))))
|
div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...tribeText(tribe, l, { zoomImages: true }))))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1871,7 +1867,7 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
||||||
? `<div class="cn-filter-row">${btn(hrefFor('', activeAuthor), i18n.all, byAuthor.length, !activeType)}${[...typeCounts.entries()].map(([k, n]) => btn(hrefFor(k, activeAuthor), kindName(byAuthor.find(it => kindKey(it) === k)), n, activeType === k)).join('')}</div>`
|
? `<div class="cn-filter-row">${btn(hrefFor('', activeAuthor), i18n.all, byAuthor.length, !activeType)}${[...typeCounts.entries()].map(([k, n]) => btn(hrefFor(k, activeAuthor), kindName(byAuthor.find(it => kindKey(it) === k)), n, activeType === k)).join('')}</div>`
|
||||||
: '';
|
: '';
|
||||||
const authorRow = authorCounts.size > 1
|
const authorRow = authorCounts.size > 1
|
||||||
? `<div class="cn-filter-row">${btn(hrefFor(activeType, ''), i18n.all, [...authorCounts.values()].reduce((a, b) => a + b, 0), !activeAuthor)}${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, id), who(id), n, activeAuthor === id)).join('')}</div>`
|
? `<div class="cn-filter-row">${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, activeAuthor === id ? '' : id), who(id), n, activeAuthor === id)).join('')}</div>`
|
||||||
: '';
|
: '';
|
||||||
const renderItem = (it) => {
|
const renderItem = (it) => {
|
||||||
const mt = it.contentType === 'media' ? String(it.mediaType || '') : '';
|
const mt = it.contentType === 'media' ? String(it.mediaType || '') : '';
|
||||||
|
|
@ -1879,9 +1875,9 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
||||||
const title = escapeHtml(it.title || '');
|
const title = escapeHtml(it.title || '');
|
||||||
const blob = cnBlobUrl(it.image);
|
const blob = cnBlobUrl(it.image);
|
||||||
const media = !blob ? ''
|
const media = !blob ? ''
|
||||||
: mt === 'image' ? `<img class="cn-tribe-img" src="${blob}" alt="${title}" loading="lazy"/>`
|
: mt === 'image' ? `<img class="post-image" src="${blob}" alt="${title}" loading="lazy"/>`
|
||||||
: mt === 'audio' ? `<audio class="cn-tribe-player" controls preload="metadata" src="${blob}"></audio>`
|
: mt === 'audio' ? `<audio class="post-audio" controls preload="metadata" src="${blob}"></audio>`
|
||||||
: mt === 'video' ? `<video class="cn-tribe-player" controls preload="metadata" src="${blob}"></video>`
|
: mt === 'video' ? `<video class="post-video" controls preload="metadata" src="${blob}"></video>`
|
||||||
: ['document', 'file', 'torrent'].includes(mt) ? `<a class="cn-tribe-link" href="${blob}" rel="noopener">⇩ ${title || escapeHtml(kind)}</a>`
|
: ['document', 'file', 'torrent'].includes(mt) ? `<a class="cn-tribe-link" href="${blob}" rel="noopener">⇩ ${title || escapeHtml(kind)}</a>`
|
||||||
: '';
|
: '';
|
||||||
const url = mt === 'bookmark' ? String(it.url || '') : '';
|
const url = mt === 'bookmark' ? String(it.url || '') : '';
|
||||||
|
|
@ -1929,8 +1925,6 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
||||||
.cn-tribe-meta{display:flex;gap:12px;flex-wrap:wrap;align-items:baseline;color:var(--fg-dim);font-size:12px;word-break:break-all}
|
.cn-tribe-meta{display:flex;gap:12px;flex-wrap:wrap;align-items:baseline;color:var(--fg-dim);font-size:12px;word-break:break-all}
|
||||||
.cn-tribe-title{color:var(--fg);font-size:18px;font-weight:700;margin:8px 0;word-break:break-word}
|
.cn-tribe-title{color:var(--fg);font-size:18px;font-weight:700;margin:8px 0;word-break:break-word}
|
||||||
.cn-tribe-text{color:var(--fg-soft);line-height:1.5;margin:8px 0;word-break:break-word}
|
.cn-tribe-text{color:var(--fg-soft);line-height:1.5;margin:8px 0;word-break:break-word}
|
||||||
.cn-tribe-img{display:block;max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);margin:10px 0}
|
|
||||||
.cn-tribe-player{display:block;width:100%;max-width:100%;margin:10px 0;border-radius:6px;background:#000}
|
|
||||||
.cn-tribe-link{display:inline-block;margin:8px 0;padding:6px 12px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);word-break:break-all}
|
.cn-tribe-link{display:inline-block;margin:8px 0;padding:6px 12px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);word-break:break-all}
|
||||||
.cn-tribe-options{margin:8px 0;padding-left:20px;color:var(--fg-soft)}
|
.cn-tribe-options{margin:8px 0;padding-left:20px;color:var(--fg-soft)}
|
||||||
.cn-tribe-replies{margin-top:12px;border-top:1px solid var(--border);padding-top:8px}
|
.cn-tribe-replies{margin-top:12px;border-top:1px solid var(--border);padding-top:8px}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
||||||
|
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||||
const { config } = require("../server/SSB_server.js");
|
const { config } = require("../server/SSB_server.js");
|
||||||
const { renderStyledText } = require("../backend/renderStyledText")
|
const { renderStyledText } = require("../backend/renderStyledText")
|
||||||
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||||
|
|
@ -55,7 +55,7 @@ const renderVideoPlayer = (videoObj) =>
|
||||||
{ class: "video-container video-container-row" },
|
{ class: "video-container video-container-row" },
|
||||||
videoHyperaxe({
|
videoHyperaxe({
|
||||||
controls: true,
|
controls: true,
|
||||||
src: `/blob/${encodeURIComponent(videoObj.url)}`,
|
src: blobUrlOf(videoObj.url),
|
||||||
preload: "metadata"
|
preload: "metadata"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
@ -232,7 +232,6 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {})
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
...(emptyMod ? [] : [
|
...(emptyMod ? [] : [
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()),
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()),
|
||||||
|
|
@ -370,7 +369,6 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
||||||
{ class: "filters" },
|
{ class: "filters" },
|
||||||
form(
|
form(
|
||||||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q }),
|
|
||||||
input({ type: "hidden", name: "sort", value: sort }),
|
input({ type: "hidden", name: "sort", value: sort }),
|
||||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []),
|
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []),
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,15 @@ const stepAction = (key, profile, status) => {
|
||||||
if (key === "ux") {
|
if (key === "ux") {
|
||||||
let cfg = {}
|
let cfg = {}
|
||||||
try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {}
|
try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {}
|
||||||
const cur = ["ainav", "chats", "feed", "phone"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks"
|
const cur = ["ainav", "chats", "feed", "phone", "rooms"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks"
|
||||||
return form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" },
|
return form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" },
|
||||||
div({ class: "welcome-ux-grid" },
|
div({ class: "welcome-ux-grid" },
|
||||||
pickCard("ux", "blocks", cur, i18n.uxModeMenus, "/assets/images/ux-blocks.png"),
|
pickCard("ux", "blocks", cur, i18n.uxModeMenus, "/assets/images/ux-blocks.png"),
|
||||||
cfg.modules?.aiNavMod === "on" ? pickCard("ux", "ainav", cur, i18n.uxModeAINav, "/assets/images/ux-ainav.png") : null,
|
cfg.modules?.aiNavMod === "on" ? pickCard("ux", "ainav", cur, i18n.uxModeAINav, "/assets/images/ux-ainav.png") : null,
|
||||||
cfg.modules?.chatsMod === "on" ? pickCard("ux", "chats", cur, i18n.uxModeChats, "/assets/images/ux-chats.png") : null,
|
cfg.modules?.chatsMod === "on" ? pickCard("ux", "chats", cur, i18n.uxModeChats, "/assets/images/ux-chats.png") : null,
|
||||||
pickCard("ux", "feed", cur, i18n.uxModeFeed, "/assets/images/ux-feed.png"),
|
pickCard("ux", "feed", cur, i18n.uxModeFeed, "/assets/images/ux-feed.png"),
|
||||||
cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null
|
cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null,
|
||||||
|
cfg.modules?.roomsMod !== "off" ? pickCard("ux", "rooms", cur, i18n.roomsTitle, "/assets/images/ux-rooms.png") : null
|
||||||
),
|
),
|
||||||
submitRow(i18n.welcomeStepUxAction)
|
submitRow(i18n.welcomeStepUxAction)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,6 @@ const renderFilters = (filter, q, params = {}, census = []) => {
|
||||||
const labelOf = (mode) => String(i18n[`wikiFilter${mode.charAt(0).toUpperCase() + mode.slice(1)}`] || mode).toUpperCase();
|
const labelOf = (mode) => String(i18n[`wikiFilter${mode.charAt(0).toUpperCase() + mode.slice(1)}`] || mode).toUpperCase();
|
||||||
return div({ class: "filters" },
|
return div({ class: "filters" },
|
||||||
form({ method: "GET", action: "/wiki", class: "ui-toolbar ui-toolbar--filters" },
|
form({ method: "GET", action: "/wiki", class: "ui-toolbar ui-toolbar--filters" },
|
||||||
input({ type: "hidden", name: "q", value: q || "" }),
|
|
||||||
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
|
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
|
||||||
...(params.emptyMod ? [] : [
|
...(params.emptyMod ? [] : [
|
||||||
...FILTERS.filter(chipVisible).map(mode => button({ type: "submit", name: "filter", value: mode, class: filter === mode ? "filter-btn active" : "filter-btn" }, labelOf(mode))),
|
...FILTERS.filter(chipVisible).map(mode => button({ type: "submit", name: "filter", value: mode, class: filter === mode ? "filter-btn active" : "filter-btn" }, labelOf(mode))),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue