Browse Source

advanced refactoring + bugs fixing

psy 1 month ago
parent
commit
1595922260
46 changed files with 6305 additions and 3997 deletions
  1. 6 0
      docs/CHANGELOG.md
  2. 0 0
      docs/devs/CONTRIBUTORS
  3. 0 0
      docs/devs/MAINTAINERS
  4. 0 0
      docs/devs/code-of-conduct.md
  5. 0 0
      docs/devs/contract.md
  6. 0 0
      docs/devs/contributing.md
  7. 0 0
      docs/devs/maintaining.md
  8. 0 16
      docs/install.md
  9. 30 0
      docs/install/install.md
  10. 10 0
      install.sh
  11. 32 0
      oasis.sh
  12. 0 77
      scripts/build.sh
  13. 0 57
      scripts/oasis.go
  14. 12 0
      scripts/patch-node-modules.js
  15. 101 114
      src/index.js
  16. 6 6
      src/supports.js
  17. 3 4
      src/updater.js
  18. 0 0
      src/assets/favicon.svg
  19. 0 0
      src/assets/snh-oasis.jpg
  20. 0 0
      src/assets/highlight.css
  21. 104 259
      src/assets/style.css
  22. 192 0
      src/client/assets/themes/Clear-SNH.css
  23. 149 0
      src/client/assets/themes/Dark-SNH.css
  24. 192 0
      src/client/assets/themes/Matrix-SNH.css
  25. 190 0
      src/client/assets/themes/Purple-SNH.css
  26. 15 0
      src/client/assets/translations/i18n.js
  27. 313 0
      src/client/assets/translations/oasis_en.js
  28. 307 0
      src/client/assets/translations/oasis_es.js
  29. 262 0
      src/client/assets/translations/oasis_fr.js
  30. 2 2
      src/ssb/cli-cmd-aliases.js
  31. 11 57
      src/ssb/index.js
  32. 11 32
      src/http/index.js
  33. 6 8
      src/cli.js
  34. 1 1
      src/config.js
  35. 4 1
      src/config.json
  36. 72 0
      src/configs/server-config.json
  37. 182 285
      src/models.js
  38. 0 174
      src/server.js
  39. 231 0
      src/server/SSB_server.js
  40. 3736 1810
      package-lock.json
  41. 6 4
      package.json
  42. 0 51
      src/ssb/flotilla.js
  43. 0 45
      src/ssb/progress.js
  44. 0 878
      src/views/i18n.js
  45. 107 85
      src/views/index.js
  46. 12 31
      src/views/markdown.js

+ 6 - 0
docs/CHANGELOG.md

@@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.3.5 - 2025-02-15
+
+### Changed
+
+- Refactoring tasks + Skin selector + Language selector
+
 ## v0.3.0 - 2024-12-15
 
 ### Changed

docs/CONTRIBUTORS → docs/devs/CONTRIBUTORS


docs/MAINTAINERS → docs/devs/MAINTAINERS


docs/code-of-conduct.md → docs/devs/code-of-conduct.md


docs/contract.md → docs/devs/contract.md


docs/contributing.md → docs/devs/contributing.md


docs/maintaining.md → docs/devs/maintaining.md


+ 0 - 16
docs/install.md

@@ -1,16 +0,0 @@
-# Install
-
-This is a guide on how to download the source code for Oasis so that you can
-build and install it on your device.
-
---------------
-
-For a GNU/Linux based system, execute the following steps (from a shell):
-
-    sudo apt-get install git curl
-    curl -sL http://deb.nodesource.com/setup_22.x | sudo bash -
-    sudo apt-get install -y nodejs
-    git clone https://code.03c8.net/KrakensLab/oasis
-    cd oasis
-    npm install .
-    npm run start

+ 30 - 0
docs/install/install.md

@@ -0,0 +1,30 @@
+# Oasis Installation Guide
+
+This guide will walk you through the process of installing **Oasis** on your device. 
+
+You can either use the automated installation script or manually download the source code.
+
+---
+
+## 1) Automated Installation (Recommended)
+
+To install **Oasis** with a single command, run: 'sh install.sh'
+
+---
+
+## 2) Manual Installation 
+
+On a GNU/Linux based system, execute the following steps (from a shell):
+
+    sudo apt-get install git curl
+    curl -sL http://deb.nodesource.com/setup_22.x | sudo bash -
+    sudo apt-get install -y nodejs
+    git clone https://code.03c8.net/KrakensLab/oasis
+    cd oasis
+    npm install .
+    
+---
+
+## 3) Run Oasis
+
+To run **Oasis** just launch: 'sh oasis.sh'

+ 10 - 0
install.sh

@@ -0,0 +1,10 @@
+#!/bin/bash
+
+cd src/server
+printf "==========================\n"
+printf "|| OASIS Installer v0.1 ||\n"
+printf "==========================\n"
+npm install .
+printf "==========================\n"
+printf "\nOASIS has been correctly deployed! ;)\n\n"
+printf "Run: 'sh oasis.sh' to start ...\n\n"

+ 32 - 0
oasis.sh

@@ -0,0 +1,32 @@
+#!/bin/bash
+
+CURRENT_DIR=$(pwd)
+
+cd "$CURRENT_DIR/src/server" || { echo "Directory not found: $CURRENT_DIR/src/server"; exit 1; }
+node SSB_server.js start &
+
+check_server_ready() {
+  local host="127.0.0.1"
+  local port="8008"
+
+  node -e "
+  const net = require('net');
+  const client = new net.Socket();
+  client.setTimeout(5000);  // Set a timeout of 5 seconds
+  client.connect($port, '$host', function() {
+    client.end();  // Successfully connected, close the socket
+    process.exit(0);  // Exit with a success code
+  });
+  client.on('error', function(err) {
+    process.exit(1);  // Exit with error code if connection fails
+  });
+  " 
+}
+
+until check_server_ready; do
+  sleep 1
+done
+
+cd "$CURRENT_DIR/src/backend" || { echo "Directory not found: $CURRENT_DIR/src/backend"; exit 1; }
+node backend.js
+

+ 0 - 77
scripts/build.sh

@@ -1,77 +0,0 @@
-#!/bin/sh
-
-set -ex
-
-BASEDIR="$(dirname "$0")"
-TARGET_VERSION="22.12.0"
-
-cd "$BASEDIR/.."
-
-git clean -fdx
-
-mkdir -p vendor
-cd vendor
-
-get_tgz () {
-  TARGET_PLATFORM="$1"
-  TARGET="node-v$TARGET_VERSION-$TARGET_PLATFORM-x64"
-  ARCHIVE="$TARGET.tar.gz"
-  URL="https://nodejs.org/dist/v$TARGET_VERSION/$ARCHIVE"
-  TARGET_NODE="$TARGET/bin/node"
-
-  wget "$URL"
-  tar -xvf "$ARCHIVE" "$TARGET_NODE"
-  rm -f "$ARCHIVE"
-}
-
-get_zip () {
-  TARGET_PLATFORM="$1"
-  TARGET="node-v$TARGET_VERSION-$TARGET_PLATFORM-x64"
-  ARCHIVE="$TARGET.zip"
-  URL="https://nodejs.org/dist/v$TARGET_VERSION/$ARCHIVE"
-  TARGET_NODE="$TARGET/node.exe"
-
-  wget "$URL"
-  unzip "$ARCHIVE" "$TARGET_NODE"
-  rm -f "$ARCHIVE"
-}
-
-get_tgz darwin
-get_tgz linux
-get_zip win
-
-cd ..
-
-# Avoid building anything from source.
-npm ci --only=prod --ignore-scripts --no-audit --no-fund
-# More trouble than it's worth :)
-rm -rf ./node_modules/sharp
-
-export GOARCH="amd64"
-
-# Darwin (shell script)
-export GOOS="darwin"
-OUTFILE="oasis-$GOOS-$GOARCH"
-go build -ldflags "-X main.node=vendor/node-v$TARGET_VERSION-darwin-x64/bin/node" -o "$OUTFILE" scripts/oasis.go
-chmod +x "$OUTFILE"
-
-# Linux (ELF executable)
-export GOOS="linux"
-OUTFILE="oasis-$GOOS-$GOARCH"
-go build -ldflags "-X main.node=vendor/node-v$TARGET_VERSION-linux-x64/bin/node" -o "$OUTFILE" scripts/oasis.go
-chmod +x "$OUTFILE"
-
-# Windows (batch file)
-export GOOS="windows"
-OUTFILE="oasis-$GOOS-$GOARCH.exe"
-go build -ldflags "-X main.node=vendor\\node-v$TARGET_VERSION-win-x64\\bin\\node" -o "$OUTFILE" scripts/oasis.go
-chmod +x "$OUTFILE"
-
-# I think if the zip already exists it's adding files to the existing archive?
-ZIP_PATH="/tmp/oasis-x64.zip"
-
-rm -f "$ZIP_PATH"
-zip -r "$ZIP_PATH" . -x ".git/**"
-
-git clean -fdx
-

+ 0 - 57
scripts/oasis.go

@@ -1,57 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-)
-
-// The relative path to the `node` binary depends on the platform, so we
-// pass this via an `-ldflags` hack I don't completely understand. In my
-// head this is similar to how GCC lets you use `-D` to define a macro to
-// be inserted by the preprocessor.
-var node string
-
-func main() {
-	// The problem with relative paths is that they only work when
-	// you run `./oasis-platform-x64`, but not when you run a command
-	// like `./path/to/oasis-platform-x64`. To resolve this problem
-	// we need to put together an absolute path, which we can build
-	// with the first argument (the relative path of this executable)
-	// and the relative path of either the `node` binary or the
-	// source code directory so that we can run `node src`.
-	node := filepath.Join(filepath.Dir(os.Args[0]), node)
-	src := filepath.Join(filepath.Dir(os.Args[0]), "src")
-
-	// We know that the command will be the absolute path to `node`
-	// and the first argument will be the absolute path to the `src`
-	// directory, but we need to get collect the rest of the arguments
-	// programatically by pulling them out of the `os.Args` slice and
-	// putting them in a new slice called `args`.
-	args := []string{src}
-	for i := 1; i < len(os.Args); i++ {
-		args = append(args, os.Args[i])
-	}
-
-	// This seems to execute the script and pass-through all of the
-	// arguments we want, *plus* it hooks up stdout and stderr, but
-	// the exit code of Oasis doesn't seem to be passed through. This
-	// is easy to test with a command like:
-	//
-	//	./oasis-platform-x64 --port -1
-	//
-	// This should give an exit code of 1, but it seems to exit 0. :/
-	cmd := exec.Command(node, args...)
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-
-	// This catches problems like "no such file or directory" if the
-	// `node` variable points to a path where there isn't a binary.
-	//
-	// TODO: I think we're supposed to handle the exit code here.
-	err := cmd.Run()
-	if err != nil {
-		fmt.Println(err)
-	}
-}

+ 12 - 0
scripts/patch-node-modules.js

@@ -0,0 +1,12 @@
+const fs = require('fs');
+const path = require('path');
+
+const ssbRefPath = path.resolve(__dirname, '../src/server/node_modules/ssb-ref/index.js');
+
+if (fs.existsSync(ssbRefPath)) {
+  const data = fs.readFileSync(ssbRefPath, 'utf8');
+  const patchedData = data.replace('exports.parseAddress = deprecate(\'ssb-ref.parseAddress\', parseAddress)', 'exports.parseAddress = parseAddress');
+
+  fs.writeFileSync(ssbRefPath, patchedData);
+  console.log('[OASIS] [PATCH] Patched ssb-ref to remove deprecated usage of parseAddress');
+}

+ 101 - 114
src/index.js

@@ -2,10 +2,9 @@
 
 "use strict";
 
-// Minimum required to get config
 const path = require("path");
-const envPaths = require("env-paths");
-const {cli} = require("./cli");
+const envPaths = require("../server/node_modules/env-paths");
+const {cli} = require("../client/oasis_client");
 const fs = require("fs");
 const os = require('os');
 const promisesFs = require("fs").promises;
@@ -62,7 +61,7 @@ try {
 
 const { get } = require("node:http");
 
-const debug = require("debug")("oasis");
+const debug = require("../server/node_modules/debug")("oasis");
 
 const log = (formatter, ...args) => {
   const isDebugEnabled = debug.enabled;
@@ -82,12 +81,11 @@ debug("Current configuration: %O", config);
 debug(`You can save the above to ${defaultConfigFile} to make \
 these settings the default. See the readme for details.`);
 
-const { saveConfig, getConfig } = require('./config');
+const { saveConfig, getConfig } = require('../configs/config-manager');
 
 const oasisCheckPath = "/.well-known/oasis";
 
 process.on("uncaughtException", function (err) {
-  // This isn't `err.code` because TypeScript doesn't like that.
   if (err["code"] === "EADDRINUSE") {
     get(url + oasisCheckPath, (res) => {
       let rawData = "";
@@ -124,31 +122,47 @@ Alternatively, you can set the default port in ${defaultConfigFile} with:
       });
     });
   } else {
-    throw err;
+    console.log("");
+    console.log("Oasis traceback (share below content with devs to report!):");
+    console.log("===========================================================");
+    console.log(err);
+    console.log("");
   }
 });
 
 process.argv = [];
 
-const http = require("./http");
+const http = require("../client/middleware");
 
-const {koaBody} = require("koa-body");
-const { nav, ul, li, a } = require("hyperaxe");
-const open = require("open");
-const pull = require("pull-stream");
-const koaRouter = require("@koa/router");
-const ssbMentions = require("ssb-mentions");
-const isSvg = require('is-svg');
-const { isFeed, isMsg, isBlob } = require("ssb-ref");
+const {koaBody} = require("../server/node_modules/koa-body");
+const { nav, ul, li, a } = require("../server/node_modules/hyperaxe");
+const open = require("../server/node_modules/open");
+const pull = require("../server/node_modules/pull-stream");
+const koaRouter = require("../server/node_modules/@koa/router");
+const ssbMentions = require("../server/node_modules/ssb-mentions");
+const isSvg = require('../server/node_modules/is-svg');
+const { isFeed, isMsg, isBlob } = require("../server/node_modules/ssb-ref");
 
-const ssb = require("./ssb");
+const ssb = require("../client/gui");
 
 const router = new koaRouter();
-const cooler = ssb({ offline: config.offline });
 
-const models = require("./models");
+const extractMentions = async (text) => {
+  const mentions = ssbMentions(text) || [];
+  const resolvedMentions = await Promise.all(mentions.map(async (mention) => {
+    const name = mention.name || await about.name(mention.link); 
+    return {
+      link: mention.link,
+      name: name || 'Anonymous', 
+    };
+  }));
+  return resolvedMentions;
+};
+
+const models = require("../models/main_models");
+const cooler = ssb({ offline: config.offline });
 
-const { about, blob, friend, meta, post, vote, wallet, legacy, cipher } = models({
+const { about, blob, friend, meta, post, vote, wallet, legacy, cipher} = models({
   cooler,
   isPublic: config.public,
 });
@@ -157,7 +171,6 @@ const nameWarmup = about._startNameWarmup();
 const preparePreview = async function (ctx) {
   let text = String(ctx.request.body.text);
   const mentions = {};
-  // TODO: sort by relationship
   const rex = /(^|\s)(?!\[)@([a-zA-Z0-9-]+)([\s.,!?)~]{1}|$)/g;
   let m;
   while ((m = rex.exec(text)) !== null) {
@@ -169,11 +182,8 @@ const preparePreview = async function (ctx) {
       mentions[name] = found;
     }
   }
-  // filter the matches depending on the follow relation
   Object.keys(mentions).forEach((name) => {
     let matches = mentions[name];
-    // if we find mention matches for a name, and we follow them / they follow us,
-    // then use those matches as suggestions
     const meaningfulMatches = matches.filter((m) => {
       return (m.rel.followsMe || m.rel.following) && m.rel.blocking === false;
     });
@@ -182,24 +192,16 @@ const preparePreview = async function (ctx) {
     }
     mentions[name] = matches;
   });
-
-  // replace the text with a markdown link if we have unambiguous match
   const replacer = (match, name, sign) => {
     let matches = mentions[name];
     if (matches && matches.length === 1) {
-      // we found an exact match, don't send it to frontend as a suggestion
-      delete mentions[name];
-      // format markdown link and put the correct sign back at the end
-      return `[@${matches[0].name}](${matches[0].feed})${sign ? sign : ""}`;
+      return `[@${matches[0].name}](${matches[0].feed})${sign || ""}`;
     }
     return match;
   };
   text = text.replace(rex, replacer);
-
-  // add blob new blob to the end of the document.
   text += await handleBlobUpload(ctx);
 
-  // author metadata for the preview-post
   const ssb = await cooler.open();
   const authorMeta = {
     id: ssb.id,
@@ -210,10 +212,6 @@ const preparePreview = async function (ctx) {
   return { authorMeta, text, mentions };
 };
 
-// handleBlobUpload ingests an uploaded form file.
-// it takes care of maximum blob size (5meg), exif stripping and mime detection.
-// finally it returns the correct markdown link for the blob depending on the mime-type.
-// it supports plain, image and also audio: and video: as understood by ssbMarkdown.
 const handleBlobUpload = async function (ctx) {
   if (!ctx.request.files) return "";
 
@@ -250,14 +248,9 @@ const handleBlobUpload = async function (ctx) {
         return clean;
       }
     };
-
     const dataString = data.toString("binary");
-    // implementation borrowed from ssb-blob-files
-    // (which operates on a slightly different data structure, sadly)
-    // https://github.com/ssbc/ssb-blob-files/blob/master/async/image-process.js
     data = Buffer.from(removeExif(dataString), "binary");
   } catch (e) {
-    // blob was likely not a jpeg -- no exif data to remove. proceeding with blob upload
   }
 
   const addBlob = new Promise((resolve, reject) => {
@@ -274,8 +267,7 @@ const handleBlobUpload = async function (ctx) {
     name: blobUpload.name,
   };
 
-  // determine encoding to add the correct markdown link
-  const FileType = require("file-type");
+  const FileType = require("../server/node_modules/file-type");
   try {
     let fileType = await FileType.fromBuffer(data);
     blob.mime = fileType.mime;
@@ -284,7 +276,6 @@ const handleBlobUpload = async function (ctx) {
     blob.mime = "application/octet-stream";
   }
 
-  // append uploaded blob as markdown to the end of the input text
   if (blob.mime.startsWith("image/")) {
     return `\n![${blob.name}](${blob.id})`;
   } else if (blob.mime.startsWith("audio/")) {
@@ -364,11 +355,11 @@ const {
   walletSendConfirmView,
   walletSendResultView,
   legacyView,
-  cipherView,
-} = require("./views/index.js");
+  cipherView
+} = require("../views/main_views");
 
-const ssbRef = require("ssb-ref");
-const markdownView = require("./views/markdown.js");
+const ssbRef = require("../server/node_modules/ssb-ref");
+const markdownView = require("../views/markdown");
 
 let sharp;
 
@@ -378,8 +369,8 @@ try {
   // Optional dependency
 }
 
-const readmePath = path.join(__dirname, "..", "README.md");
-const packagePath = path.join(__dirname, "..", "package.json");
+const readmePath = path.join(__dirname, "..", ".." ,"README.md");
+const packagePath = path.join(__dirname, "..", "server", "package.json");
 
 const readme = fs.readFileSync(readmePath, "utf8");
 const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
@@ -419,35 +410,33 @@ router
     ctx.body = "oasis";
   })
   .get("/public/popular/:period", async (ctx) => {
-    const { period } = ctx.params; 
-    const popularMod = ctx.cookies.get("popularMod") || 'on';
-    if (popularMod !== 'on') {
-      ctx.redirect('/modules');
-      return;
-    }
-    const publicPopular = async ({ period }) => {
-      const messages = await post.popular({ period });
-      const selectedLanguage = ctx.cookies.get("language") || "en";
-      const i18nBase = require("./views/i18n");
-      let i18n = i18nBase[selectedLanguage];
-      exports.setLanguage = (language) => {
-        selectedLanguage = language;
-        i18n = Object.assign({}, i18nBase.en, i18nBase[language]);
-      };
-      const prefix = nav(
-        ul(
-          a({ href: "./day" }, i18n.day),
-          a({ href: "./week" }, i18n.week),
-          a({ href: "./month" }, i18n.month),
-          a({ href: "./year" }, i18n.year)
-        )
-      );
-      return popularView({
-        messages,
-        prefix,
-      });
-    };
-    ctx.body = await publicPopular({ period });
+  const { period } = ctx.params;
+  const popularMod = ctx.cookies.get("popularMod") || 'on';
+
+  if (popularMod !== 'on') {
+    ctx.redirect('/modules');
+    return;
+  }
+  const i18n = require("../client/assets/translations/i18n");
+  const lang = ctx.cookies.get('lang') || 'en'; 
+  const translations = i18n[lang] || i18n['en']; 
+
+  const publicPopular = async ({ period }) => {
+    const messages = await post.popular({ period });
+    const prefix = nav(
+      ul(
+        a({ href: "./day" }, translations.day),
+        a({ href: "./week" }, translations.week),
+        a({ href: "./month" }, translations.month),
+        a({ href: "./year" }, translations.year)
+      )
+    );
+    return popularView({
+      messages,
+      prefix,
+    });
+  }
+  ctx.body = await publicPopular({ period });
   })
   .get("/public/latest", async (ctx) => {
     const latestMod = ctx.cookies.get("latestMod") || 'on';
@@ -661,10 +650,6 @@ router
     // This prevents an auto-download when visiting the URL.
     ctx.attachment(blobId, { type: "inline" });
 
-    // If we don't do this explicitly the browser downloads the SVG and thinks
-    // that it's plain XML, so it doesn't render SVG files correctly. Note that
-    // this library is **not a full SVG parser**, and may cause false positives
-    // in the case of malformed XML like `<svg><div></svg>`.
     if (isSvg(buffer)) {
       ctx.type = "image/svg+xml";
     }
@@ -756,7 +741,7 @@ router
     ctx.body = modulesView({ popularMod, topicsMod, summariesMod, latestMod, threadsMod, multiverseMod, inboxMod, invitesMod, walletMod, legacyMod, cipherMod });
   })
   .get("/settings", async (ctx) => {
-    const theme = ctx.cookies.get("theme") || config.theme;
+    const theme = ctx.cookies.get("theme") || "Dark-SNH";
     const getMeta = async ({ theme }) => {
       return settingsView({
         theme,
@@ -809,16 +794,10 @@ router
     };
     ctx.body = await likes({ feed });
   })
-  .get("/settings/readme", async (ctx) => {
-    const status = async (text) => {
-      return markdownView({ text });
-    };
-    ctx.body = await status(readme);
-  })
   .get("/mentions", async (ctx) => {
     const mentions = async () => {
       const messages = await post.mentionsMe();
-      return mentionsView({ messages });
+      return mentionsView({ messages }); 
     };
     ctx.body = await mentions();
   })
@@ -850,7 +829,6 @@ router
     const { message } = ctx.params;
     const thread = async (message) => {
       const messages = await post.fromThread(message);
-      debug("got %i messages", messages.length);
       return threadView({ messages });
     };
     ctx.body = await thread(message);
@@ -947,8 +925,7 @@ router
 
     const publishSubtopic = async ({ message, text }) => {
       // TODO: rename `message` to `parent` or `ancestor` or similar
-      const mentions = ssbMentions(text) || undefined;
-
+      const mentions = extractMentions(text);
       const parent = await post.get(message);
       return post.subtopic({
         parent,
@@ -984,8 +961,7 @@ router
       rawContentWarning.length > 0 ? rawContentWarning : undefined;
 
     const publishComment = async ({ message, text }) => {
-      // TODO: rename `message` to `parent` or `ancestor` or similar
-      const mentions = ssbMentions(text) || undefined;
+      const mentions = extractMentions(text);
       const parent = await meta.get(message);
 
       return post.comment({
@@ -1008,11 +984,10 @@ router
     const text = String(ctx.request.body.text);
     const rawContentWarning = String(ctx.request.body.contentWarning);
     const contentWarning =
-      rawContentWarning.length > 0 ? rawContentWarning : undefined;
+    rawContentWarning.length > 0 ? rawContentWarning : undefined;
 
     const publish = async ({ text, contentWarning }) => {
-      const mentions = ssbMentions(text) || undefined;
-
+      const mentions = await extractMentions(text); 
       return post.root({
         text,
         mentions,
@@ -1194,12 +1169,22 @@ router
     const referer = new URL(ctx.request.header.referer);
     ctx.redirect(referer.href);
   })
-  .post("/theme.css", koaBody(), async (ctx) => {
-    const theme = "SNH-Oasis"; 
-    ctx.cookies.set("theme", theme);  
-    const referer = new URL(ctx.request.header.referer);
-    ctx.redirect(referer.href);  
-  })
+  .post("/settings/theme", koaBody(), async (ctx) => {
+    const theme = String(ctx.request.body.theme);
+    const currentConfig = getConfig();
+    if (theme) {
+        currentConfig.themes.current = theme;
+        const configPath = path.join(__dirname, '../configs', 'oasis-config.json');
+        fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
+        ctx.cookies.set("theme", theme);
+        ctx.redirect("/settings");
+    } else {
+        currentConfig.themes.current = "Dark-SNH";
+        fs.writeFileSync(path.join(__dirname, 'configs', 'oasis-config.json'), JSON.stringify(currentConfig, null, 2));
+        ctx.cookies.set("theme", "Dark-SNH");
+        ctx.redirect("/settings");
+     }
+   })
   .post("/language", koaBody(), async (ctx) => {
     const language = String(ctx.request.body.language);
     ctx.cookies.set("language", language);
@@ -1227,8 +1212,6 @@ router
       const invite = String(ctx.request.body.invite);
       await meta.acceptInvite(invite);
     } catch (e) {
-      // Just in case it's an invalid invite code. :(
-      debug(e);
     }
     ctx.redirect("/invites");
   })
@@ -1361,16 +1344,20 @@ const middleware = [
       0
     );
     const totalTarget = status.sync.since * values.length;
-
     const left = totalTarget - totalCurrent;
-
     const percent = Math.floor((totalCurrent / totalTarget) * 1000) / 10;
-    const mebibyte = 1024 * 1024;
-
-    if (left > mebibyte) {
+    const megabyte = 1024 * 1024;
+    if (left > megabyte) {
       ctx.response.body = indexingView({ percent });
     } else {
-      await next();
+       try {
+    await next();
+  } catch (err) {
+    ctx.status = err.status || 500;
+    ctx.body = { message: err.message || 'Internal Server Error' };
+    // Optionally log the error for debugging
+    console.error(err);
+  }
     }
   },
   routes,

+ 6 - 6
src/supports.js

@@ -10,11 +10,11 @@ const {
   a,
   br,
   li,
-} = require("hyperaxe");
+} = require("../server/node_modules/hyperaxe");
 
-const envPaths = require("env-paths");
-const {cli} = require("./cli");
-const ssb = require("./ssb");
+const envPaths = require("../server/node_modules/env-paths");
+const {cli} = require("../client/oasis_client");
+const ssb = require("../client/gui");
 
 const defaultConfig = {};
 const defaultConfigFile = join(
@@ -28,7 +28,7 @@ if (config.debug) {
 }
 const cooler = ssb({ offline: config.offline });
 
-const models = require("./models.js");
+const models = require("../models/main_models");
 
 const { about } = models({
   cooler,
@@ -155,4 +155,4 @@ async function getNameByIdRecommended(recommended){
 
 module.exports.supporting = supports;
 module.exports.blocking = blocks;
-module.exports.recommending = recommends
+module.exports.recommending = recommends

+ 3 - 4
src/updater.js

@@ -1,8 +1,8 @@
-const fetch = require('node-fetch');
+const fetch = require('../server/node_modules/node-fetch');
 const { existsSync, readFileSync } = require('fs');
 const { join } = require('path');
 
-const localpackage = join(__dirname, '../package.json');
+const localpackage = join(__dirname, '../server/package.json');
 const remoteUrl = 'https://code.03c8.net/KrakensLab/oasis/raw/master/package.json'; // Official SNH-Oasis
 const remoteUrl2 = 'https://raw.githubusercontent.com/epsylon/oasis/main/package.json'; // Mirror SNH-Oasis
 
@@ -67,7 +67,7 @@ async function checkMirror(callback) {
 }
 
 exports.getRemoteVersion = async () => {
-  if (existsSync('.git')) { 
+  if (existsSync('../../.git')) { 
     try {
       const response = await fetch(remoteUrl, {
         method: 'GET',
@@ -85,7 +85,6 @@ exports.getRemoteVersion = async () => {
       if (!response.ok) {
         throw new Error(`Request failed with status ${response.status}`);
       }
-
       const data = await response.text();
       diffVersion(data, (status) => {
         if (status === "required") {

+ 0 - 0
src/assets/favicon.svg


+ 0 - 0
src/assets/snh-oasis.jpg


+ 0 - 0
src/assets/highlight.css


+ 104 - 259
src/assets/style.css

@@ -1,3 +1,4 @@
+/* General Reset */
 * {
   user-select: none;
 }
@@ -7,7 +8,7 @@
 }
 
 span {
- padding: 2px;
+  padding: 2px;
 }
 
 hr {
@@ -17,7 +18,8 @@ hr {
   width: 100%;
 }
 
-textarea {
+/* Textarea, input, select styles */
+textarea, input, select {
   background-color: #444;
   color: #FFA500;
   border: 1px solid #333;
@@ -26,13 +28,13 @@ textarea {
   width: 100%;
   font-size: 1em;
   margin-top: 10px;
-  height: 150px;
 }
 
-input, textarea, select {
-  user-select: text;
+input[type="text"], input[type="email"], input[type="password"], textarea {
+  margin-bottom: 15px;
 }
 
+/* Body and overall page styles */
 html {
   color: #FFA500;
 }
@@ -47,6 +49,7 @@ body {
   flex-direction: column;
 }
 
+/* Link styles */
 a {
   color: #FFA500;
   text-decoration: underline;
@@ -57,15 +60,53 @@ a:hover {
 }
 
 .header {
-  width: 100%;
+  display: flex;
+  justify-content: space-between; 
+  padding: 10px;
+  align-items: center; 
+}
+
+form-button-group {
+  display: flex;
+  gap: 10px;
+  flex-wrap: nowrap;
+  justify-content: start;
+}
+
+.conn-buttons {
+  display: table-row;
+  flex-direction: column;
+  justify-content: space-between; 
+  padding: 10px;
+  align-items: center; 
+}
+
+.left-bar {
   display: flex;
   align-items: center;
-  background-color: #222;
-  padding: 10px 20px;
-  box-sizing: border-box;
-  border-bottom: 1px solid #444;
 }
 
+.right-bar {
+  display: flex;
+  align-items: center;
+}
+
+.search-input {
+  margin-left: 10px;
+  width: 400px; 
+  height: 32px;
+}
+
+nav ul {
+  list-style-type: none;
+  padding: 0;
+}
+
+nav ul li {
+  margin: 5px 0;
+}
+
+/* Logo and Navigation */
 .logo-icon {
   width: 50px;
   height: auto;
@@ -104,21 +145,25 @@ nav ul li a:hover {
   text-decoration: underline;
 }
 
+/* Main content area */
 .main-content {
   display: flex;
   justify-content: space-between;
   width: 100%;
   padding: 15px 20px;
   box-sizing: border-box;
+  flex-wrap: wrap;
 }
 
 .sidebar-left, .sidebar-right {
   background-color: #222;
-  width: 20%;
   display: flex;
   height: 100%;
   box-sizing: border-box;
   border: 1px solid #444;
+  padding: 25px;
+  border-radius: 0;
+  width: 10%;
 }
 
 .sidebar-left nav ul,
@@ -136,13 +181,29 @@ nav ul li a:hover {
 .main-column {
   background-color: #333;
   padding: 20px;
-  flex: 3;
+  flex-grow: 1;
   border-radius: 5px;
   border: 1px solid #333;
   box-sizing: border-box;
   overflow: hidden;
+  width: 70%;
+  margin-left: 5px;
+  margin-right: 5px;
 }
 
+.main-content {
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  padding: 15px 20px;
+  box-sizing: border-box;
+}
+
+.main-column {
+  flex-grow: 1;
+}
+
+/* Section styles */
 section {
   margin-bottom: 20px;
   color: #FFA500;
@@ -154,9 +215,10 @@ ul {
 }
 
 ul li {
-  margin: 10px 0; 
+  margin: 10px 0;
 }
 
+/* Buttons */
 button, input[type="submit"], input[type="button"] {
   background-color: #FFA500;
   color: #000;
@@ -166,7 +228,7 @@ button, input[type="submit"], input[type="button"] {
   cursor: pointer;
   font-size: 0.9em;
   transition: all 0.3s ease;
-  margin-top: 10px;
+  margin-top: 4.5px;
 }
 
 button:hover, input[type="submit"]:hover, input[type="button"]:hover {
@@ -174,21 +236,7 @@ button:hover, input[type="submit"]:hover, input[type="button"]:hover {
   border-color: #FFD700;
 }
 
-input, textarea {
-  background-color: #444;
-  color: #FFA500;
-  border: 1px solid #333;
-  padding: 10px;
-  border-radius: 5px;
-  width: 100%;
-  font-size: 1em;
-  margin-top: 10px;
-}
-
-input[type="text"], input[type="email"], input[type="password"], textarea {
-  margin-bottom: 15px;
-}
-
+/* Tables */
 table {
   width: 100%;
   border-collapse: collapse;
@@ -210,6 +258,7 @@ table tr:nth-child(even) {
   background-color: #444;
 }
 
+/* Media queries */
 @media (max-width: 768px) {
   .main-content {
     flex-direction: column;
@@ -240,6 +289,7 @@ table tr:nth-child(even) {
   }
 }
 
+/* Avatar and profile styles */
 div {
   margin-bottom: 20px;
   padding: 20px;
@@ -250,127 +300,16 @@ div {
   overflow: hidden;
 }
 
-.sidebar-left, .sidebar-right {
-  background-color: #333;
-  padding: 25px;
-  border-radius: 0;
-  width: 10%;
-  box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2);
-}
-
-.main-column {
-  background-color: #333;
-  padding: 25px;
-  width: 60%;
-  border-radius: 0;
-  border: 1px solid #444;
-  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-}
-
-.header {
-  width: 100%;
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-  background-color: #222;
-  padding: 15px;
-  margin-bottom: 20px;
-}
-
-section {
-  margin-bottom: 25px;
-  padding: 20px;
-  border-radius: 10px;
-  background-color: #333;
-  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-}
-
-section h1 {
-  margin-bottom: 15px;
-  color: #FFA500;
-}
-
-section p {
-  color: #FFF;
-}
-
-@media (max-width: 768px) {
-  .main-column {
-    width: 100%;
-  }
-
-  .sidebar-left, .sidebar-right {
-    width: 100%;
-    margin-bottom: 20px;
-  }
-
-  .header {
-    padding: 10px;
-  }
-}
-
-.form-button-group {
-  display: flex;
-  gap: 10px;
-  justify-content: center;
-  flex-wrap: wrap;
-}
-
-.conn-buttons {
-  display: flex;
-  gap: 10px;
-  justify-content: flex-start;
-}
-
-button {
-  margin: 0;
-}
-
-.message {
-  padding: 20px;
-  background-color: #333;
-  border-radius: 8px;
-  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-  color: #FFA500;
-  max-width: 600px;
-  margin: 0 auto;
-}
-
-.hljs-string, .hljs-link {
-  user-select: text;
-}
-
-.author {
-  display: inline-block;
-  align-items: center; 
-  padding: 3px;
-}
-
-.send-by-label, {
-  margin-left: 3px;
-  margin-right: 3px;
-}
-
-.author-action {
-  display: inline-block;
-}
-
-.time {
-  margin-right: 3px;
-}
-
-label {
-  font-weight: normal;
+div .header-content{
+  width: 35%;
 }
 
 .avatar-profile {
-  width: 40px;  
-  height: 40px;  
-  align-items: center; 
-  vertical-align: middle;
-  margin-right: 8px; 
   width: 75px;
   height: 75px;
+  align-items: center;
+  vertical-align: middle;
+  margin-right: 8px;
 }
 
 .avatar {
@@ -390,9 +329,9 @@ h1 {
   font-size: 1.5em;
   font-weight: bold;
   margin: 20px 0;
-  word-wrap: break-word; 
-  overflow-wrap: break-word; 
-  white-space: normal; 
+  word-wrap: break-word;
+  overflow-wrap: break-word;
+  white-space: normal;
   user-select: text;
 }
 
@@ -400,8 +339,14 @@ article {
   font-size: 1.1em;
   color: #FFF;
   margin-bottom: 20px;
-  font-style: italic; 
-  text-align: center; 
+}
+
+.thread-view {
+ width: 90%;
+}
+
+thread-container {
+ width: 95%;
 }
 
 .profile {
@@ -432,9 +377,14 @@ article {
   font-weight: bold;
 }
 
+footer {
+  clear: both;
+  margin-top: 20px;
+  padding: 10px 0;
+}
+
 footer div {
   display: flex;
-  align-items: center;
   gap: 10px;
 }
 
@@ -472,12 +422,7 @@ footer .btn:hover {
   background-color: #007B9F;
 }
 
-footer span {
-  font-weight: bold;
-  text-align: center;
-  display: block; 
-}
-
+/* Post preview image styles */
 .post-preview img {
   max-width: 100%;
   height: auto;
@@ -493,6 +438,7 @@ footer span {
   margin: 0 auto;
 }
 
+/* Section, article, and message word-wrap */
 section, article, .message, .profile {
   word-wrap: break-word;
   overflow-wrap: break-word;
@@ -507,111 +453,10 @@ article img {
   margin: 0 auto;
 }
 
+/* Action container */
 .action-container {
   display: flex;
   flex-direction: column;
   align-items: center;
-  gap: 20px;
-}
-
-.action-button {
-  padding: 12px 20px;
-  font-size: 16px;
-  background-color: #ff6700;
-  color: white;
-  border: none;
-  border-radius: 5px;
-  cursor: pointer;
-  transition: background-color 0.3s ease;
-  width: 100%;
-  max-width: 300px;
 }
 
-.action-button:hover {
-  background-color: #e65c00;
-}
-
-#import-button {
-  background-color: #4caf50;
-}
-
-#import-button:hover {
-  background-color: #45a049;
-}
-
-#import-button:focus + #import-file {
-  display: block;
-}
-
-.cipher-result {
-  margin-top: 20px;
-  padding: 10px;
-  border: 1px solid #ddd;
-  background-color: #000;
-  color: #FFA500;
-  border-radius: 5px;
-  visibility: hidden; 
-  opacity: 0; 
-  transition: opacity 0.5s ease, visibility 0s 0.5s; 
-  user-select: text;
-}
-
-.cipher-result.visible {
-  visibility: visible;
-  opacity: 1;
-  transition: opacity 0.5s ease;
-}
-
-.encrypted-result {
-  color: #FFA500;
-  font-weight: bold;
-}
-
-.decrypted-result {
-  color: #FFA500;
-  font-weight: bold;
-}
-
-.wallet-form-button-group-center {
-  display: flex;
-  flex-direction: row;
-  gap: 10px;
-}
-
-.wallet-balance {
-  font-size: 2em;
-  color: #FFA500;
-  font-weight: bold;
-  margin: 0;
-  border-radius: 5px;
-  transition: opacity 0.5s ease, visibility 0s 0.5s; 
-  user-select: text;
-}
-
-.form-button-group-center {
-  display: flex;
-  flex-direction: row;
-  gap: 10px;
-}
-
-summary {
-  font-size: 1.5em;
-  font-weight: bold;
-  text-align: left; 
-  margin-bottom: 10px;
-  padding: 5px 0; 
-  user-select: text;
-}
-
-article.content {
-  text-align: left;
-  padding-left: 20px;
-  user-select: text;
-}
-
-article.content p {
-  font-size: 1em;
-  line-height: 1.6;
-  margin-bottom: 15px;
-  user-select: text;
-}

+ 192 - 0
src/client/assets/themes/Clear-SNH.css

@@ -0,0 +1,192 @@
+body {
+  background-color: #F9F9F9;
+  color: #2C2C2C;
+  font-family: 'Roboto', sans-serif;
+}
+
+header, footer {
+  background-color: #FFFFFF;
+  border-bottom: 1px solid #E0E0E0;
+}
+
+.sidebar-left, .sidebar-right {
+  background-color: #F4F4F4;
+  border: 1px solid #E0E0E0;
+}
+
+.main-column {
+  background-color: #FFFFFF;
+  border: 1px solid #E0E0E0;
+}
+
+button, input[type="submit"], input[type="button"] {
+  background-color: #FF6F00;
+  color: #FFFFFF;
+  border: none;
+  border-radius: 6px;
+  padding: 10px 20px;
+  cursor: pointer;
+  font-weight: 600;
+}
+
+button:hover, input[type="submit"]:hover, input[type="button"]:hover {
+  background-color: #FF8F00;
+}
+
+input, textarea, select {
+  background-color: #FFFFFF;
+  color: #2C2C2C;
+  border: 1px solid #E0E0E0;
+  border-radius: 4px;
+  padding: 8px;
+  font-size: 16px;
+}
+
+a {
+  color: #007BFF;
+  text-decoration: none;
+}
+
+a:hover {
+  text-decoration: underline;
+}
+
+table {
+  background-color: #FFFFFF;
+  color: #2C2C2C;
+  width: 100%;
+  border-collapse: collapse;
+}
+
+table th {
+  background-color: #F8F8F8;
+  padding: 12px 15px;
+  text-align: left;
+  font-weight: 600;
+}
+
+table tr:nth-child(even) {
+  background-color: #FAFAFA;
+}
+
+table td {
+  padding: 12px 15px;
+}
+
+nav ul {
+  background-color: #FFFFFF;
+  border-bottom: 1px solid #E0E0E0;
+  padding: 0;
+  margin: 0;
+  list-style-type: none;
+}
+
+nav ul li {
+  display: inline-block;
+  margin-right: 10px;
+}
+
+nav ul li a {
+  color: #2C2C2C;
+  padding: 12px 15px;
+  font-size: 16px;
+  display: inline-block;
+  transition: background-color 0.3s;
+}
+
+nav ul li a:hover {
+  background-color: #F4F4F4;
+  border-radius: 4px;
+}
+
+.profile {
+  background-color: #FFFFFF;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.profile .name {
+  color: #FF6F00;
+  font-size: 20px;
+  font-weight: 700;
+}
+
+.avatar {
+  border: 3px solid #FF6F00;
+  border-radius: 50%;
+  width: 60px;
+  height: 60px;
+}
+
+article, section {
+  background-color: #FFFFFF;
+  color: #2C2C2C;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+}
+
+.post-preview img {
+  border-radius: 8px;
+  max-width: 100%;
+  height: auto;
+}
+
+.post-preview .image-container {
+  max-width: 100%;
+  overflow: hidden;
+  display: block;
+  margin: 0 auto;
+}
+
+div {
+  background-color: #FFFFFF;
+  border: 1px solid #E0E0E0;
+}
+
+div .header-content {
+  width: 100%;
+}
+
+::-webkit-scrollbar {
+  width: 8px;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #B0B0B0;
+  border-radius: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #F9F9F9;
+}
+
+.action-container {
+  background-color: #FFFFFF;
+  border: 1px solid #E0E0E0;
+  padding: 20px;
+  border-radius: 8px;
+  color: #2C2C2C;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+}
+
+footer {
+  background-color: #FFFFFF;
+  border-top: 1px solid #E0E0E0;
+  padding: 15px 0;
+}
+
+footer a {
+  background-color: #007BFF;
+  color: #FFFFFF;
+  padding: 10px 20px;
+  border-radius: 6px;
+  text-decoration: none;
+  font-weight: 600;
+}
+
+footer a:hover {
+  background-color: #0056b3;
+}
+

+ 149 - 0
src/client/assets/themes/Dark-SNH.css

@@ -0,0 +1,149 @@
+body {
+  background-color: #121212;
+  color: #FFD700;
+}
+
+header, footer {
+  background-color: #1F1F1F;
+}
+
+.sidebar-left, .sidebar-right {
+  background-color: #1A1A1A;
+  border: 1px solid #333;
+}
+
+.main-column {
+  background-color: #1C1C1C;
+}
+
+button, input[type="submit"], input[type="button"] {
+  background-color: #444;
+  color: #FFD700;
+  border: 1px solid #444;
+}
+
+button:hover, input[type="submit"]:hover, input[type="button"]:hover {
+  background-color: #333;
+  border-color: #666;
+}
+
+input, textarea, select {
+  background-color: #333;
+  color: #FFD700;
+  border: 1px solid #555;
+}
+
+a {
+  color: #FFD700;
+}
+
+a:hover {
+  color: #FFDD44;
+}
+
+table {
+  background-color: #222;
+  color: #FFD700;
+}
+
+table th {
+  background-color: #333;
+}
+
+table tr:nth-child(even) {
+  background-color: #2A2A2A;
+}
+
+nav ul {
+  background-color: #1F1F1F;
+}
+
+nav ul li a:hover {
+  color: #FFDD44;
+  text-decoration: underline;
+}
+
+.profile {
+  background-color: #222;
+  padding: 15px;
+  border-radius: 8px;
+}
+
+.profile .name {
+  color: #FFD700;
+}
+
+.avatar {
+  border: 3px solid #FFD700;
+}
+
+article, section {
+  background-color: #1C1C1C;
+  color: #FFD700;
+}
+
+.article img {
+  border: 3px solid #FFD700;
+}
+
+.post-preview img {
+  border: 3px solid #FFD700;
+}
+
+.post-preview .image-container {
+  max-width: 100%;
+  overflow: hidden;
+  display: block;
+  margin: 0 auto;
+}
+
+div {
+  background-color: #1A1A1A;
+  border: 1px solid #333;
+}
+
+div .header-content {
+  width: 100%;
+}
+
+::-webkit-scrollbar {
+  width: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #444;
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #222;
+}
+
+.action-container {
+  background-color: #1A1A1A;
+  border: 1px solid #333;
+  padding: 10px;
+  border-radius: 8px;
+  color: #FFD700;
+}
+
+footer {
+  background-color: #1F1F1F;
+  border-top: 1px solid #333;
+  padding: 10px 0;
+}
+
+footer a {
+  background-color: #444;
+  color: #FFD700;
+  text-align: center;
+  padding: 8px 16px;
+  border-radius: 5px;
+  text-decoration: none;
+}
+
+footer a:hover {
+  background-color: #333;
+  color: #FFDD44;
+}
+

+ 192 - 0
src/client/assets/themes/Matrix-SNH.css

@@ -0,0 +1,192 @@
+body {
+  background-color: #000000;
+  color: #00FF00;
+  font-family: 'Courier New', monospace;
+  line-height: 1.5;
+  font-size: 16px;
+}
+
+header, footer {
+  background-color: #000000;
+  color: #00FF00;
+  padding: 20px;
+  text-align: center;
+  border-bottom: 2px solid #00FF00;
+  font-size: 18px;
+}
+
+footer {
+  border-top: 2px solid #00FF00;
+}
+
+.sidebar-left, .sidebar-right {
+  background-color: #000000;
+  color: #00FF00;
+  border-right: 2px solid #00FF00;
+  padding: 15px;
+  margin-bottom: 10px;
+}
+
+.main-column {
+  background-color: #000000;
+  color: #00FF00;
+  padding: 20px;
+  border-left: 2px solid #00FF00;
+  margin-bottom: 20px;
+}
+
+button, input[type="submit"], input[type="button"] {
+  background-color: #000000;
+  color: #00FF00;
+  border: 2px solid #00FF00;
+  border-radius: 8px;
+  padding: 10px 20px;
+  cursor: pointer;
+  font-family: 'Courier New', monospace;
+  text-transform: none;
+  font-size: 16px;
+}
+
+button:hover, input[type="submit"]:hover, input[type="button"]:hover {
+  background-color: #00FF00;
+  color: #000000;
+  border-color: #00FF00;
+  box-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
+}
+
+input, textarea, select {
+  background-color: #1A1A1A;
+  color: #00FF00;
+  border: 1px solid #00FF00;
+  border-radius: 5px;
+  padding: 10px;
+  font-family: 'Courier New', monospace;
+  font-size: 16px;
+}
+
+a {
+  color: #00FF00;
+  text-decoration: none;
+  font-weight: normal;
+  font-size: 16px;
+}
+
+table {
+  background-color: #000000;
+  color: #00FF00;
+  width: 100%;
+  border: 1px solid #00FF00;
+  font-size: 16px;
+}
+
+table th {
+  background-color: #00FF00;
+  color: #000000;
+}
+
+table tr:nth-child(even) {
+  background-color: #1A1A1A;
+}
+
+nav ul {
+  background-color: #000000;
+  padding: 0;
+  margin: 0;
+  border-bottom: 2px solid #00FF00;
+}
+
+nav ul li {
+  display: inline-block;
+  margin-right: 10px;
+}
+
+nav ul li a {
+  color: #00FF00;
+  padding: 10px;
+  display: inline-block;
+  font-family: 'Courier New', monospace;
+  text-transform: none;
+  font-size: 16px;
+}
+
+.profile {
+  background-color: #1A1A1A;
+  color: #00FF00;
+  padding: 20px;
+  border-radius: 8px;
+  border: 1px solid #00FF00;
+  box-shadow: 0 2px 5px rgba(0,255,0,0.5);
+}
+
+.profile .name {
+  color: #00FF00;
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.avatar {
+  border: 4px solid #00FF00;
+  border-radius: 50%;
+  width: 80px;
+  height: 80px;
+  margin-bottom: 10px;
+}
+
+article, section {
+  background-color: #1A1A1A;
+  color: #00FF00;
+  padding: 20px;
+  border-radius: 10px;
+  border: 1px solid #00FF00;
+  box-shadow: 0 4px 10px rgba(0, 255, 0, 0.5);
+}
+
+.post-preview {
+  background-color: #00FF00;
+  padding: 15px;
+  border-radius: 8px;
+  color: #000000;
+}
+
+.post-preview img {
+  border-radius: 8px;
+  max-width: 100%;
+}
+
+.post-preview .image-container {
+  display: block;
+  margin: 0 auto;
+  max-width: 100%;
+  overflow: hidden;
+}
+
+::-webkit-scrollbar {
+  width: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #00FF00;
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #000000;
+}
+
+.action-container {
+  background-color: #1A1A1A;
+  border: 2px solid #00FF00;
+  padding: 20px;
+  border-radius: 8px;
+  color: #00FF00;
+  box-shadow: 0 2px 5px rgba(0, 255, 0, 0.5);
+}
+
+footer a {
+  background-color: #00FF00;
+  color: #000000;
+  padding: 8px 20px;
+  border-radius: 5px;
+  text-decoration: none;
+}
+

+ 190 - 0
src/client/assets/themes/Purple-SNH.css

@@ -0,0 +1,190 @@
+body {
+  background-color: #4B0A6D;
+  color: #E5E5E5;
+  font-family: 'Arial', sans-serif;
+}
+
+header, footer {
+  background-color: #7A1F9C;
+  color: #E5E5E5;
+  padding: 20px;
+  text-align: center;
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
+}
+
+footer {
+  border-top: 2px solid #9B1C96;
+}
+
+.sidebar-left, .sidebar-right {
+  background-color: #39006D;
+  border: 1px solid #9B1C96;
+  color: #E5E5E5;
+  padding: 15px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+}
+
+.main-column {
+  background-color: #1A1A1A;
+  border: 1px solid #9B1C96;
+  padding: 20px;
+  color: #E5E5E5;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+}
+
+button, input[type="submit"], input[type="button"] {
+  background-color: #9B1C96;
+  color: #E5E5E5;
+  border: 2px solid #6A0066;
+  border-radius: 10px;
+  padding: 12px 24px;
+  cursor: pointer;
+}
+
+button:hover, input[type="submit"]:hover, input[type="button"]:hover {
+  background-color: #6A0066;
+  border-color: #9B1C96;
+  box-shadow: 0 0 15px rgba(155, 28, 150, 0.8);
+}
+
+input, textarea, select {
+  background-color: #333333;
+  color: #E5E5E5;
+  border: 1px solid #9B1C96;
+  border-radius: 5px;
+  padding: 12px;
+}
+
+input:focus, textarea:focus, select:focus {
+  background-color: #39006D;
+  outline: none;
+}
+
+a {
+  color: #9B1C96;
+  text-decoration: none;
+}
+
+a:hover {
+  color: #E5E5E5;
+}
+
+table {
+  background-color: #1A1A1A;
+  color: #E5E5E5;
+  width: 100%;
+  border-collapse: collapse;
+}
+
+table th {
+  background-color: #6A0066;
+}
+
+table tr:nth-child(even) {
+  background-color: #333333;
+}
+
+nav ul {
+  background-color: #39006D;
+  padding: 0;
+  margin: 0;
+  list-style: none;
+}
+
+nav ul li {
+  display: inline-block;
+  margin-right: 10px;
+}
+
+nav ul li a {
+  color: #E5E5E5;
+  padding: 15px;
+  display: inline-block;
+  text-transform: none;
+  font-weight: bold;
+  letter-spacing: 0;
+}
+
+.profile {
+  background-color: #333333;
+  color: #E5E5E5;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.3);
+}
+
+.profile .name {
+  color: #9B1C96;
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.avatar {
+  border: 4px solid #9B1C96;
+  border-radius: 50%;
+  width: 80px;
+  height: 80px;
+  margin-bottom: 10px;
+}
+
+article, section {
+  background-color: #333333;
+  color: #E5E5E5;
+  padding: 20px;
+  border-radius: 10px;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.3);
+}
+
+.post-preview {
+  background-color: #39006D;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
+}
+
+.post-preview img {
+  border-radius: 8px;
+  max-width: 100%;
+}
+
+.post-preview .image-container {
+  display: block;
+  margin: 0 auto;
+  max-width: 100%;
+  overflow: hidden;
+}
+
+::-webkit-scrollbar {
+  width: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #9B1C96;
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #1A1A1A;
+}
+
+.action-container {
+  background-color: #333333;
+  border: 2px solid #9B1C96;
+  padding: 20px;
+  border-radius: 8px;
+  color: #E5E5E5;
+  box-shadow: 0 2px 5px rgba(0,0,0,0.5);
+}
+
+footer a {
+  background-color: #9B1C96;
+  color: #FFFFFF;
+  padding: 8px 20px;
+  border-radius: 5px;
+  text-decoration: none;
+}
+
+footer a:hover {
+  background-color: #6A0066;
+}
+

+ 15 - 0
src/client/assets/translations/i18n.js

@@ -0,0 +1,15 @@
+const path = require('path');
+let i18n = {};
+const languages = ['en', 'es', 'fr']; // Add more language codes if needed
+
+languages.forEach(language => {
+  try {
+    const languagePath = path.join(__dirname, `oasis_${language}.js`);
+    const languageData = require(languagePath);
+    i18n[language] = languageData[language];
+  } catch (error) {
+    console.error(`Failed to load language file for ${language}:`, error);
+  }
+});
+
+module.exports = i18n;

+ 313 - 0
src/client/assets/translations/oasis_en.js

@@ -0,0 +1,313 @@
+const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
+module.exports = {
+  en: {
+    languageName: "English",
+  extended: "Multiverse",
+  extendedDescription: [
+    "When you support someone you may download posts from the inhabitants they support, and those posts show up here, sorted by recency.",
+  ],
+  popular: "Highlights",
+  popularDescription: [
+    "Posts from inhabitants in your network, ",
+    strong("sorted by spreads"),
+    ". Select the period of time, to get a list.",
+  ],
+    day: "Day",
+    week: "Week",
+    month: "Month",
+    year: "Year",
+    latest: "Latest",
+    latestDescription: [
+      strong("Posts"),
+      " from yourself and inhabitants you support, sorted by recency.",
+    ],
+    topics: "Topics",
+    topicsDescription: [
+      strong("Topics"),
+      " from yourself and inhabitants you support, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
+    ],
+    summaries: "Summaries",
+    summariesDescription: [
+      strong("Topics and some comments"),
+      " from yourself and inhabitants you support, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
+    ],
+    threads: "Threads",
+    threadsDescription: [
+      strong("Posts that have comments"),
+      " from inhabitants you support and your multiverse, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
+    ],
+    profile: "Avatar",
+    inhabitants: "Inhabitants", 
+    manualMode: "Manual Mode",
+    mentions: "Mentions",
+    mentionsDescription: [
+      strong("Posts that @mention you"),
+      ", sorted by recency.",
+    ],
+    private: "Inbox",
+    peers: "Peers",
+    privateDescription: [
+      "The latest comment from ",
+      strong("private threads that include you"),
+      ", sorted by recency. Private posts are encrypted for your public key, and have a maximum of 7 recipients. Recipients cannot be added after the thread has started. Select the timestamp to view the full thread.",
+    ],
+    search: "Search",
+    imageSearch: "Image Search",
+    searchPlaceholder: "Seek for inhabitants, #hashtags and keywords...",
+    settings: "Settings",
+    continueReading: "continue reading",
+    moreComments: "more comment",
+    readThread: "read the rest of the thread",    
+    // modules
+    modules: "Modules",
+    modulesViewTitle: "Modules",
+    modulesViewDescription: "Set your environment by enabling or disabling modules.",
+    inbox: "Inbox",
+    multiverse: "Multiverse",
+    popularLabel: "⌘ Highlights",
+    topicsLabel: "ϟ Topics",
+    latestLabel: "☄ Latest",
+    summariesLabel: "※ Summaries",
+    threadsLabel: "♺ Threads",
+    multiverseLabel: "∞ Multiverse",
+    inboxLabel: "☂ Inbox",
+    invitesLabel: "ꔹ Invites",
+    walletLabel: "❄ Wallet",
+    legacyLabel: "ꖤ Keys",
+    cipherLabel: "ꗄ Crypter",
+    saveSettings: "Save configuration",
+    // post actions
+    comment: "Comment",
+    subtopic: "Subtopic",
+    json: "JSON",
+    // relationships
+    unfollow: "Unsupport",
+    follow: "Support",
+    block: "Block",
+    unblock: "Unblock",
+    newerPosts: "Newer posts",
+    olderPosts: "Older posts",
+    feedRangeEmpty: "The given range is empty for this feed. Try viewing the ",
+    seeFullFeed: "full feed",
+    feedEmpty: "The Oasis client has never seen posts from this account.",
+    beginningOfFeed: "This is the beginning of the feed",
+    noNewerPosts: "No newer posts have been received yet.",
+    relationshipNotFollowing: "You are not supported",
+    relationshipTheyFollow: "They support",
+    relationshipMutuals: "Mutual support",
+    relationshipFollowing: "You are supporting",
+    relationshipYou: "You",
+    relationshipBlocking: "You are blocking",
+    relationshipNone: "You are not supporting",
+    relationshipConflict: "Conflict",
+    relationshipBlockingPost: "Blocked post",
+    // spreads view
+    viewLikes: "View spreads",
+    spreadedDescription: "List of posts spread by the inhabitant.",
+    likedBy: " -> Spreads",
+    // composer
+    attachFiles: "Attach files",
+    mentionsMatching: "Matching mentions",
+    preview: "Preview",
+    publish: "Write",
+    contentWarningPlaceholder: "Add a subject to the post (optional)",
+    privateWarningPlaceholder: "Add inhabitants to send a private post (ex: @bob @alice) (optional)",
+    publishWarningPlaceholder: "...",
+    publishCustomDescription: [
+      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
+    ],
+    commentWarning: [
+      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
+    ],
+    commentPublic: "public",
+    commentPrivate: "private",
+    commentLabel: ({ publicOrPrivate, markdownUrl }) => [
+    ],
+    publishLabel: ({ markdownUrl, linkTarget }) => [
+      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
+    ],
+    replyLabel: ({ markdownUrl }) => [
+      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
+    ],
+    publishCustomInfo: ({ href }) => [
+      "If you have experience, you can also ",
+      a({ href }, "write an advanced post"),
+      ".",
+    ],
+    publishBasicInfo: ({ href }) => [
+      "If you have not experience, you should ",
+      a({ href }, "write a post"),
+      ".",
+    ],
+    publishCustom: "Write advanced post",
+    subtopicLabel: ({ markdownUrl }) => [
+      "Create a ",
+      strong("public subtopic"),
+      " of this post with ",
+      a({ href: markdownUrl }, "Markdown"),
+      ". Posts cannot be edited or deleted. To respond to an entire thread, select ",
+      strong("comment"),
+      " instead. Preview shows attached media.",
+    ],
+    // settings
+    updateit: "Get updates",
+    info: "Info",
+    settingsIntro: ({ version }) => [
+      `Oasis: [${version}]`,
+    ],
+    timeAgo: "ago",
+    sendTime: "about ",
+    theme: "Theme",
+    legacy: "Keys",
+    legacyTitle: "Keys",
+    legacyDescription: "Manage your secret (private key) quickly and safely.",   
+    legacyExportButton: "Export",
+    legacyImportButton: "Import",
+    exportTitle: "Export data",
+    exportDescription: "Set password (min 32 characters long) to encrypt your key",
+    importTitle: "Import data",
+    importDescription: "Import your encrypted secret (private key) to enable your avatar",
+    importAttach: "Attach encrypted file (.enc)",    
+    passwordLengthInfo: "Password must be at least 32 characters long.",
+    passwordImport: "Write your password to decrypt data that will be saved at your system home (name: secret)",
+    randomPassword: "Random password",
+    exportPasswordPlaceholder: "Use lowercase, uppercase, numbers & symbols",
+    fileInfo: "Your encrypted secret key will be saved at your system home (name: oasis.enc)",
+    themeIntro:
+      "Choose a theme.",
+    setTheme: "Set theme",
+    language: "Language",
+    languageDescription:
+      "If you'd like to use another language, select it here.",
+    setLanguage: "Set language",
+    status: "Status",
+    peerConnections: "Peers",
+    peerConnectionsIntro: "Manage all your connections with other peers.",
+    online: "Online",
+    supported: "Supported",
+    recommended: "Recommended", 
+    blocked: "Blocked",
+    noConnections: "No peers connected.",
+    noSupportedConnections: "No peers supported.",
+    noBlockedConnections: "No peers blocked.",
+    noRecommendedConnections: "No peers recommended.",
+    connectionActionIntro:
+      "",
+    startNetworking: "Start networking",
+    stopNetworking: "Stop networking",
+    restartNetworking: "Restart networking",
+    sync: "Sync network",
+    indexes: "Indexes",
+    indexesDescription:
+      "Rebuilding your indexes is safe, and may fix some types of bugs.",
+    invites: "Invites",
+    invitesDescription:
+      "Use the PUB's invite codes here.",
+    acceptInvite: "Accept invite",
+    acceptedInvites: "Federated Networks",
+    noInvites: "No invites accepted.",
+    // search page
+    searchLabel: "Seek inhabitants and keywords, among the posts you have downloaded.",
+    // image search page
+    imageSearchLabel: "Enter words to search for images labelled with them.",
+    // posts and comments
+    commentDescription: ({ parentUrl }) => [
+      " commented on ",
+      a({ href: parentUrl }, " thread"),
+    ],
+    commentTitle: ({ authorName }) => [`Comment on @${authorName}'s post`],
+    subtopicDescription: ({ parentUrl }) => [
+      " created a subtopic from ",
+      a({ href: parentUrl }, " a post"),
+    ],
+    subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`],
+    mysteryDescription: "posted a mysterious post",
+    // misc
+    oasisDescription: "SNH Project Network",
+    submit: "Submit",
+    subjectLabel: "Subject",
+    editProfile: "Edit Avatar",
+    editProfileDescription:
+      "",
+    profileName: "Avatar name (plain text)",
+    profileImage: "Avatar image",
+    profileDescription: "Avatar description (Markdown)",
+    hashtagDescription:
+      "Posts from inhabitants in your network that reference this #hashtag, sorted by recency.",
+    rebuildName: "Rebuild database",
+    wallet: "Wallet",
+    walletAddress: "Address",
+    walletAmount: "Amount",
+    walletAddressLine: ({ address }) => `Address: ${address}`,
+    walletAmountLine: ({ amount }) => `Amount: ${amount} ECO`,
+    walletBack: "Back",
+    walletBalanceTitle: "Balance",
+    walletWalletSendTitle: "Send",
+    walletReceiveTitle: "Receive",
+    walletHistoryTitle: "History",
+    walletBalanceLine: ({ balance }) => `${balance} ECO`,
+    walletCnfrs: "Cnfrs",
+    walletConfirm: "Confirm",
+    walletDescription: "Manage your digital assets, including sending and receiving ECOin, viewing your balance, and accessing your transaction history.",
+    walletDate: "Date",
+    walletFee: "Fee (The higher the fee, the faster your transaction will be processed)",
+    walletFeeLine: ({ fee }) => `Fee: ECO ${fee}`,
+    walletHistory: "History",
+    walletReceive: "Receive",
+    walletReset: "Reset",
+    walletSend: "Send",
+    walletStatus: "Status",
+    walletDisconnected: "ECOin wallet disconnected. Check your wallet settings or connection status.",
+    walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`,
+    walletSettingsTitle: "Wallet",
+    walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
+    walletStatusMessages: {
+      invalid_amount: "Invalid amount",
+      invalid_dest: "Invalid destination address",
+      invalid_fee: "Invalid fee",
+      validation_errors: "Validation errors",
+      send_tx_success: "Transaction successful",
+    },
+    walletTitle: "Wallet",
+    walletTotalCostLine: ({ totalCost }) => `Total cost: ECO ${totalCost}`,
+    walletTransactionId: "Transaction ID",
+    walletTxId: "Tx ID",
+    walletType: "Type",
+    walletUser: "Username",
+    walletPass: "Password",
+    walletConfiguration: "Set wallet",
+    //cipher
+    cipher: "Crypter",
+    cipherTitle: "Cipher",
+    cipherDescription: "Encrypt and decrypt your text symmetrically (using a shared password).",
+    randomPassword: "Random Password",
+    cipherEncryptTitle: "Encrypt Text",
+    cipherEncryptDescription: "Set password (min 32 characters long) to encrypt your text",
+    cipherTextLabel: "Text to Encrypt",
+    cipherTextPlaceholder: "Enter text to encrypt...",
+    cipherPasswordLabel: "Password",
+    cipherPasswordPlaceholder: "Enter a password...",
+    cipherEncryptButton: "Encrypt",
+    cipherDecryptTitle: "Decrypt Text",
+    cipherDecryptDescription: "Enter the encrypted text and password to decrypt.",
+    cipherEncryptedTextLabel: "Encrypted Text",
+    cipherEncryptedTextPlaceholder: "Enter the encrypted text...",
+    cipherIvLabel: "IV",
+    cipherIvPlaceholder: "Enter the initialization vector...",
+    cipherDecryptButton: "Decrypt",
+    password: "Password",
+    text: "Text",
+    encryptedText: "Encrypted Text",
+    iv: "Initialization Vector (IV)",
+    encryptTitle: "Encrypt your text",
+    encryptDescription: "Enter the text you want to encrypt and provide a password.",
+    encryptButton: "Encrypt",
+    decryptTitle: "Decrypt your text",
+    decryptDescription: "Enter the encrypted text and provide the same password used for encryption.",
+    decryptButton: "Decrypt",
+    passwordLengthError: "Password must be at least 32 characters long.",
+    missingFieldsError: "Text, password or IV not provided.",
+    encryptionError: "Error encrypting text.",
+    decryptionError: "Error decrypting text."
+    }
+};

+ 307 - 0
src/client/assets/translations/oasis_es.js

@@ -0,0 +1,307 @@
+const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
+module.exports = {
+  es: {
+    languageName: "Español",
+  extended: "Multiverso",
+  extendedDescription: [
+    "Cuando apoyas a alguien, puedes descargar las publicaciones de los habitantes que apoyan, y esas publicaciones aparecerán aquí, ordenadas por reciente.",
+  ],
+  popular: "Destacados",
+  popularDescription: [
+    "Publicaciones de habitantes en tu red, ",
+     strong("ordenadas por apoyos"),
+    ". Selecciona el período de tiempo para obtener una lista.",
+  ],
+    day: "Día",
+    week: "Semana",
+    month: "Mes",
+    year: "Año",
+    latest: "Últimos",
+    latestDescription: [
+    strong("Publicaciones"),
+    " tuyas y de lass habitantes que apoyas, ordenadas por la más reciente.",
+    ],
+    topics: "Temas",
+    topicsDescription: [
+    strong("Temas"),
+    " tuyos y de las habitantes que apoyas, ordenados por los más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
+    ],
+    summaries: "Resúmenes",
+    summariesDescription: [
+    strong("Temas y algunos comentarios"),
+    " tuyos y de las habitantes que apoyas, ordenados por los más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
+    ],
+    threads: "Hilos",
+    threadsDescription: [
+    strong("Publicaciones que tienen comentarios"),
+    " de las habitantes que apoyas y tu multiverso, ordenadas por las más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
+    ],
+    profile: "Avatar",
+    inhabitants: "Habitantes",
+    manualMode: "Modo Manual",
+    mentions: "Menciones",
+    mentionsDescription: [
+    strong("Publicaciones que te @mencionan"),
+    ", ordenadas por las más recientes.",
+    ],
+    private: "Buzón",
+    peers: "Nodos",
+    privateDescription: [
+    "El último comentario de ",
+    strong("hilos privados que te incluyen"),
+    ", ordenados por los más recientes. Las publicaciones privadas están cifradas para tu clave pública, y tienen un máximo de 7 destinatarios. Los destinatarios no pueden ser añadidos después de que el hilo haya comenzado. Selecciona la marca de tiempo para ver el hilo completo.",
+    ],
+    search: "Buscar",
+    imageSearch: "Búsqueda de imágenes",
+    searchPlaceholder: "Busca habitantes, #hashtags and palabras clave...",
+    settings: "Configuración",
+    continueReading: "seguir leyendo",
+    moreComments: "comentarios má",
+    readThread: "leer el resto del hilo",
+    // modules
+    modules: "Módulos",
+    modulesViewTitle: "Módulos",
+    modulesViewDescription: "Configura tu entorno habilitando o deshabilitando módulos.",
+    inbox: "Buzón",
+    multiverse: "Multiverso",
+    popularLabel: "⌘ Destacados",
+    topicsLabel: "ϟ Temas",
+    latestLabel: "☄ Últimos",
+    summariesLabel: "※ Resúmenes",
+    threadsLabel: "♺ Hilos",
+    multiverseLabel: "∞ Multiverso",
+    inboxLabel: "☂ Buzón",
+    invitesLabel: "ꔹ Invitaciones",
+    walletLabel: "❄ Billetera",
+    legacyLabel: "ꖤ Llaves",
+    cipherLabel: "ꗄ Cripta",
+    saveSettings: "Guardar configuración",
+    // post actions
+    comment: "Comentar",
+    subtopic: "Subtema",
+    json: "JSON",
+    // relationships
+    unfollow: "Dejar de apoyar",
+    follow: "Apoyar",
+    block: "Bloquear",
+    unblock: "Desbloquear",
+    newerPosts: "Publicaciones más recientes",
+    olderPosts: "Publicaciones más antiguas",
+    feedRangeEmpty: "El rango dado está vacío para este feed. Intenta ver el ",
+    seeFullFeed: "feed completo",
+    feedEmpty: "El cliente local de Oasis no ha accedido a publicaciones aún.",
+    beginningOfFeed: "Este es el comienzo del feed",
+    noNewerPosts: "No se han recibido publicaciones más recientes.",
+    relationshipNotFollowing: "No te da apoyo",
+    relationshipTheyFollow: "Ellos apoyan",
+    relationshipMutuals: "Apoyo mutuo",
+    relationshipFollowing: "Das apoyo",
+    relationshipYou: "Tú",
+    relationshipBlocking: "Estás bloqueando",
+    relationshipNone: "No estás apoyando",
+    relationshipConflict: "En conflicto",
+    relationshipBlockingPost: "Publicación bloqueada",
+    // spreads view
+    viewLikes: "Ver Apoyos",
+    spreadedDescription: "Lista de publicaciones apoyadas por la habitante.",
+    likedBy: " -> Apoyos",
+    // composer
+    attachFiles: "Adjuntar archivos",
+    mentionsMatching: "Menciones coincidentes",
+    preview: "Vista previa",
+    publish: "Escribir",
+    contentWarningPlaceholder: "Agrega un tema a la publicación (opcional)",
+    privateWarningPlaceholder: "Agrega habitantes para enviar una publicación privada (ej: @bob @alice) (opcional)",
+    publishWarningPlaceholder: "...",
+    publishCustomDescription: [
+    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
+    ],
+    commentWarning: [
+    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
+    ],
+    commentPublic: "público",
+    commentPrivate: "privado",
+    commentLabel: ({ publicOrPrivate, markdownUrl }) => [
+    ],
+    publishLabel: ({ markdownUrl, linkTarget }) => [
+    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
+    ],
+    replyLabel: ({ markdownUrl }) => [
+    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
+    ],
+    publishCustomInfo: ({ href }) => [
+    "Si tienes experiencia, también puedes ",
+    a({ href }, "escribir una publicación avanzada"),
+    ".",
+    ],
+    publishBasicInfo: ({ href }) => [
+      "Si no tienes experiencia, deberías ",
+      a({ href }, "escribir una publicación"),
+      ".",
+    ],
+    publishCustom: "Escribir publicación avanzada",
+    subtopicLabel: ({ markdownUrl }) => [
+      "Crear un ",
+      strong("subtema público"),
+      " de esta publicación con ",
+      a({ href: markdownUrl }, "Markdown"),
+      ". Las publicaciones no pueden ser editadas ni eliminadas. Para responder a un hilo completo, selecciona ",
+      strong("comentar"),
+    " en su lugar. La vista previa muestra los medios adjuntos.",
+    ],
+    // settings
+    updateit: "Obtener actualizaciones",
+    info: "Información",
+    settingsIntro: ({ version }) => [
+      `Oasis: [${version}]`,
+    ],
+    timeAgo: "",
+    sendTime: "aproximadamente hace ",
+    theme: "Tema",
+    legacy: "Llaves",
+    legacyTitle: "Llaves",
+    legacyDescription: "Gestiona tus llaves (clave privada) de forma rápida y segura.",
+    legacyExportButton: "Exportar",
+    legacyImportButton: "Importar",
+    exportTitle: "Exportar datos",
+    exportDescription: "Establece una contraseña (mínimo 32 caracteres) para cifrar tu clave",
+    importTitle: "Importar datos",
+    importDescription: "Importa tu secreto cifrado (clave privada) para habilitar tu avatar",
+    importAttach: "Adjuntar archivo cifrado (.enc)",
+    passwordLengthInfo: "La contraseña debe tener al menos 32 caracteres.",
+    passwordImport: "Escribe tu contraseña para descifrar los datos que se guardarán en el directorio de tu sistema (nombre: secreto)",
+    exportPasswordPlaceholder: "Usa minúsculas, mayúsculas, números y símbolos",
+    fileInfo: "Tu clave secreta cifrada se guardará en el directorio de tu sistema (nombre: oasis.enc)",
+    themeIntro: "Elige un tema.",
+    setTheme: "Establecer tema",
+    language: "Idioma",
+    languageDescription: "Si deseas usar otro idioma, selecciona aquí.",
+    setLanguage: "Establecer idioma",
+    status: "Estado",
+    peerConnections: "Nodos",
+    peerConnectionsIntro: "Maneja todas tus conexiones que otros nodos.",
+    online: "En línea",
+    supported: "Apoyado",
+    recommended: "Recomendado",
+    blocked: "Bloqueado",
+    noConnections: "No hay habitantes conectados.",
+    noSupportedConnections: "No hay habitantes apoyados.",
+    noBlockedConnections: "No hay habitantes bloqueados.",
+    noRecommendedConnections: "No hay habitantes recomendados.",
+    connectionActionIntro: "",
+    startNetworking: "Comenzar a conectar",
+    stopNetworking: "Dejar de conectar",
+    restartNetworking: "Reiniciar la conexión",
+    sync: "Sincronizar red",
+    indexes: "Índices",
+    indexesDescription: "Reconstruir tus índices es seguro y puede solucionar algunos tipos de errores.",
+    invites: "Invitaciones",
+    invitesDescription: "Usa los códigos de invitación del PUB aquí.",
+    acceptInvite: "Aceptar invitación",
+    acceptedInvites: "Redes federadas",
+    noInvites: "No se han aceptado invitaciones",
+    // search page
+    searchLabel: "Buscar habitantes y palabras clave entre las publicaciones que has descargado.",
+    // image search page
+    imageSearchLabel: "Introduce palabras para buscar imágenes etiquetadas con ellas.",
+    // posts and comments
+    commentDescription: ({ parentUrl }) => [
+    " comentó en ",
+    a({ href: parentUrl }, " el hilo"),
+    ],
+    commentTitle: ({ authorName }) => [`Comentario en la publicación de @${authorName}`],
+    subtopicDescription: ({ parentUrl }) => [
+    " creó un subtema de ",
+    a({ href: parentUrl }, " una publicación"),
+    ],
+    subtopicTitle: ({ authorName }) => [`Subtema en la publicación de @${authorName}`],
+    mysteryDescription: "envió una publicación misteriosa",
+    // misc
+    oasisDescription: "Red del Proyecto SNH",
+    submit: "Enviar",
+    subjectLabel: "Asunto",
+    editProfile: "Editar Avatar",
+    editProfileDescription: "",
+    profileName: "Nombre del avatar (texto plano)",
+    profileImage: "Imagen del avatar",
+    profileDescription: "Descripción del avatar (Markdown)",
+    hashtagDescription: "Publicaciones de habitantes en tu red que mencionan este #hashtag, ordenadas por las más recientes.",
+    rebuildName: "Reconstruir base de datos",
+    wallet: "Billetera",
+    walletAddress: "Dirección",
+    walletAmount: "Cantidad",
+    walletAddressLine: ({ address }) => `Dirección: ${address}`,
+    walletAmountLine: ({ amount }) => `Cantidad: ${amount} ECO`,
+    walletBack: "Volver",
+    walletBalanceTitle: "Balance",
+    walletReceiveTitle: "Recibir",
+    walletHistoryTitle: "Historial",
+    walletWalletSendTitle: "Enviar",
+    walletBalanceLine: ({ balance }) => `${balance} ECO`,
+    walletCnfrs: "Cnfrs",
+    walletConfirm: "Confirmar",
+    walletDescription: "Gestiona tus activos digitales, incluyendo el envío y recepción de ECOin, ver tu saldo e historial de transacciones.",
+    walletDate: "Fecha",
+    walletFee: "Comisión (Cuanto mayor es la comisión, más rápido se procesará tu transacción)",
+    walletFeeLine: ({ fee }) => `Comisión: ECO ${fee}`,
+    walletHistory: "Historial",
+    walletReceive: "Recibir",
+    walletReset: "Restablecer",
+    walletSend: "Enviar",
+    walletStatus: "Estado",
+    walletDisconnected: "Billetera ECOin desconectada. Verifica la configuración de tu billetera o el estado de la conexión.",
+    walletSentToLine: ({ destination, amount }) => `Enviado ECO ${amount} a ${destination}`,
+    walletSettingsTitle: "Billetera",
+    walletSettingsDescription: "Integra Oasis con tu billetera ECOin.",
+    walletStatusMessages: {
+    invalid_amount: "Cantidad inválida",
+    invalid_dest: "Dirección de destino inválida",
+    invalid_fee: "Comisión inválida",
+    validation_errors: "Errores de validación",
+    send_tx_success: "Transacción exitosa",
+    },
+    walletTitle: "Billetera",
+    walletTotalCostLine: ({ totalCost }) => `Costo total: ECO ${totalCost}`,
+    walletTransactionId: "ID de transacción",
+    walletTxId: "ID Tx",
+    walletType: "Tipo",
+    walletUser: "Nombre de usuario",
+    walletPass: "Contraseña",
+    walletConfiguration: "Configurar billetera",
+    //cipher
+    cipher: "Cripta",
+    randomPassword: "Contraseña aleatoria",
+    password: "Contraseña",
+    text: "Texto",
+    encryptedText: "Texto cifrado",
+    iv: "Vector de Inicialización (IV)",
+    encryptTitle: "Cifra tu texto",
+    encryptDescription: "Introduce el texto que deseas cifrar y proporciona una contraseña.",
+    encryptButton: "Cifrar",
+    decryptTitle: "Descifra tu texto",
+    decryptDescription: "Introduce el texto cifrado y proporciona la misma contraseña utilizada para cifrar.",
+    decryptButton: "Descifrar",
+    passwordLengthError: "La contraseña debe tener al menos 32 caracteres.",
+    missingFieldsError: "Texto, contraseña o IV no proporcionados.",
+    encryptionError: "Error cifrando el texto.",
+    decryptionError: "Error descifrando el texto.",
+    //cipher
+    cipherTitle: "Cifrado",
+    cipherDescription: "Cifra y descifra contenido simétricamente (usando una contraseña compartida).",
+    randomPassword: "Contraseña Aleatoria",
+    cipherEncryptTitle: "Encriptar Texto",
+    cipherEncryptDescription: "Establece una contraseña (mínimo 32 caracteres) para cifrar tu texto",
+    cipherTextLabel: "Texto a Encriptar",
+    cipherTextPlaceholder: "Introduce el texto para encriptar...",
+    cipherPasswordLabel: "Contraseña",
+    cipherPasswordPlaceholder: "Introduce una contraseña...",
+    cipherEncryptButton: "Encriptar",
+    cipherDecryptTitle: "Desencriptar Texto",
+    cipherDecryptDescription: "Introduce el texto encriptado, la contraseña y el IV para desencriptar.",
+    cipherEncryptedTextLabel: "Texto Encriptado",
+    cipherEncryptedTextPlaceholder: "Introduce el texto encriptado...",
+    cipherIvLabel: "IV",
+    cipherIvPlaceholder: "Introduce el vector de inicialización...",
+    cipherDecryptButton: "Desencriptar"
+    }
+};

+ 262 - 0
src/client/assets/translations/oasis_fr.js

@@ -0,0 +1,262 @@
+const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
+module.exports = {
+  fr: {
+    languageName: "Français",
+  extended: "Multivers",
+  extendedDescription: [
+    "Lorsque vous soutenez quelqu'un, vous pouvez télécharger les publications des habitants qu'ils soutiennent, et ces publications apparaissent ici, triées par récence.",
+  ],
+  popular: "Points forts",
+  popularDescription: [
+    "Publications des habitants de votre réseau, ",
+    strong("triées par spreads"),
+    ". Sélectionnez la période de temps pour obtenir une liste.",
+  ],
+    day: "Jour",
+    week: "Semaine",
+    month: "Mois",
+    year: "Année",
+    latest: "Derniers",
+    latestDescription: [
+    strong("Publications"),
+    " de vous et des habitants que vous soutenez, triées par les plus récentes.",
+    ],
+    topics: "Sujets",
+    topicsDescription: [
+    strong("Sujets"),
+    " de vous et des habitants que vous soutenez, triés par les plus récents. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
+    ],
+    summaries: "Résumés",
+    summariesDescription: [
+    strong("Sujets et quelques commentaires"),
+    " de vous et des habitants que vous soutenez, triés par les plus récents. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
+    ],
+    threads: "Fils",
+    threadsDescription: [
+    strong("Publications avec commentaires"),
+    " des habitants que vous soutenez et de votre multivers, triées par les plus récentes. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
+    ],
+    profile: "Avatar",
+    inhabitants: "Habitants",
+    manualMode: "Mode Manuel",
+    mentions: "Mentions",
+    mentionsDescription: [
+    strong("Publications qui vous @mentionnent"),
+    ", triées par les plus récentes.",
+    ],
+    private: "Boîte de réception",
+    peers: "Nœuds",
+    privateDescription: [
+    "Le dernier commentaire de ",
+    strong("fils privés qui vous incluent"),
+    ", triés par les plus récents. Les publications privées sont chiffrées pour votre clé publique et ont un maximum de 7 destinataires. Les destinataires ne peuvent pas être ajoutés après le début du fil. Sélectionnez l'horodatage pour voir le fil complet.",
+    ],
+    search: "Rechercher",
+    imageSearch: "Recherche d'images",
+    searchPlaceholder: "Recherchez des habitants, des #hashtags et des mots clés ...",
+    settings: "Paramètres",
+    continueReading: "continuer la lecture",
+    moreComments: "plus de commentaires",
+    readThread: "lire le reste du fil",
+    // modules
+    modules: "Modules",
+    modulesViewTitle: "Modules",
+    modulesViewDescription: "Configurez votre environnement en activant ou désactivant des modules.",
+    inbox: "Boîte de réception",
+    multiverse: "Multivers",
+    popularLabel: "⌘ En vedette",
+    topicsLabel: "ϟ Sujets",
+    latestLabel: "☄ Derniers",
+    summariesLabel: "※ Résumés",
+    threadsLabel: "♺ Fils",
+    multiverseLabel: "∞ Multivers",
+    inboxLabel: "☂ Boîte de réception",
+    invitesLabel: "ꔹ Invitations",
+    walletLabel: "❄ Portefeuille",
+    legacyLabel: "ꖤ Clés",
+    cipherLabel: "ꗄ Crypte",
+    saveSettings: "Enregistrer les paramètres",
+    // post actions
+    comment: "Commenter",
+    subtopic: "Sous-sujet",
+    json: "JSON",
+    // relationships
+    unfollow: "Ne plus soutenir",
+    follow: "Soutenir",
+    block: "Bloquer",
+    unblock: "Débloquer",
+    newerPosts: "Publications plus récentes",
+    olderPosts: "Publications plus anciennes",
+    feedRangeEmpty: "La plage donnée est vide pour ce flux. Essayez de voir le ",
+    seeFullFeed: "flux complet",
+    feedEmpty: "Le client local d'Oasis n'a pas encore accédé aux publications.",
+    beginningOfFeed: "Ceci est le début du flux",
+    noNewerPosts: "Aucune publication plus récente reçue.",
+    relationshipNotFollowing: "Ne vous soutient pas",
+    relationshipTheyFollow: "Ils soutiennent",
+    relationshipMutuals: "Soutien mutuel",
+    relationshipFollowing: "Vous soutenez",
+    relationshipYou: "Vous",
+    relationshipBlocking: "Vous bloquez",
+    relationshipNone: "Vous ne soutenez pas",
+    relationshipConflict: "En conflit",
+    relationshipBlockingPost: "Publication bloquée",
+    // spreads view
+    viewLikes: "Voir les soutiens",
+    spreadedDescription: "Liste des publications soutenues par l'habitant.",
+    likedBy: " -> Soutiens",
+    // composer
+    attachFiles: "Joindre des fichiers",
+    mentionsMatching: "Mentions correspondantes",
+    preview: "Aperçu",
+    publish: "Publier",
+    contentWarningPlaceholder: "Ajoutez un sujet à la publication (optionnel)",
+    privateWarningPlaceholder: "Ajoutez des habitants pour envoyer une publication privée (ex: @bob @alice) (optionnel)",
+    publishWarningPlaceholder: "...",
+    publishCustomDescription: [
+    "RAPPEL : En raison de la technologie blockchain, une fois publié, il ne peut être ni modifié ni supprimé.",
+    ],
+    commentWarning: [
+    "RAPPEL : En raison de la technologie blockchain, une fois publié, il ne peut être ni modifié ni supprimé.",
+    ],
+    commentPublic: "public",
+    commentPrivate: "privé",
+    // settings
+    updateit: "Obtenir les mises à jour",
+    info: "Informations",
+    settingsIntro: ({ version }) => [
+      `Oasis: [${version}]`,
+    ],
+    timeAgo: "",
+    sendTime: "environ ",
+    theme: "Thème",
+    legacy: "Clés",
+    legacyTitle: "Clés",
+    legacyDescription: "Gérez votre secret (clé privée) rapidement et en toute sécurité.",
+    legacyExportButton: "Exporter",
+    legacyImportButton: "Importer",
+    exportTitle: "Exporter les données",
+    exportDescription: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre clé",
+    importTitle: "Importer des données",
+    importDescription: "Importez votre secret chiffré (clé privée) pour activer votre avatar",
+    importAttach: "Joindre un fichier chiffré (.enc)",
+    passwordLengthInfo: "Le mot de passe doit contenir au moins 32 caractères.",
+    passwordImport: "Saisissez votre mot de passe pour déchiffrer les données qui seront enregistrées dans le répertoire de votre système (nom : secret)",
+    exportPasswordPlaceholder: "Utilisez des minuscules, majuscules, chiffres et symboles",
+    fileInfo: "Votre clé secrète chiffrée sera enregistrée dans le répertoire de votre système (nom : oasis.enc)",
+    themeIntro: "Choisissez un thème.",
+    setTheme: "Définir le thème",
+    language: "Langue",
+    languageDescription: "Si vous souhaitez utiliser une autre langue, sélectionnez-la ici.",
+    setLanguage: "Définir la langue",
+    status: "Statut",
+    peerConnections: "Nœuds",
+    peerConnectionsIntro: "Gérez toutes vos connexions avec d'autres nœuds.",
+    online: "En ligne",
+    supported: "Soutenu",
+    recommended: "Recommandé",
+    blocked: "Bloqué",
+    noConnections: "Aucun habitant connecté.",
+    noSupportedConnections: "Aucun habitant soutenu.",
+    noBlockedConnections: "Aucun habitant bloqué.",
+    noRecommendedConnections: "Aucun habitant recommandé.",
+    startNetworking: "Commencer à se connecter",
+    stopNetworking: "Arrêter de se connecter",
+    restartNetworking: "Redémarrer la connexion",
+    sync: "Synchroniser le réseau",
+    indexes: "Index",
+    indexesDescription: "Reconstruire vos index est sûr et peut résoudre certains types d'erreurs.",
+    invites: "Invitations",
+    invitesDescription: "Utilisez les codes d'invitation du PUB ici.",
+    acceptInvite: "Accepter l'invitation",
+    acceptedInvites: "Réseaux fédérés",
+    noInvites: "Aucune invitation acceptée",
+    // misc
+    oasisDescription: "Réseau du Projet SNH",
+    submit: "Envoyer",
+    subjectLabel: "Sujet",
+    editProfile: "Modifier l'avatar",
+    editProfileDescription: "",
+    profileName: "Nom de l'avatar (texte brut)",
+    profileImage: "Image de l'avatar",
+    profileDescription: "Description de l'avatar (Markdown)",
+    hashtagDescription: "Publications des habitants de votre réseau mentionnant ce #hashtag, triées par les plus récentes.",
+    rebuildName: "Reconstruire la base de données",
+    wallet: "Portefeuille",
+    walletAddress: "Adresse",
+    walletAmount: "Montant",
+    walletAddressLine: ({ address }) => `Adresse : ${address}`,
+    walletAmountLine: ({ amount }) => `Montant : ${amount} ECO`,
+    walletBack: "Retour",
+    walletBalanceTitle: "Solde",
+    walletReceiveTitle: "Recevoir",
+    walletHistoryTitle: "Historique",
+    walletWalletSendTitle: "Envoyer",
+    walletBalanceLine: ({ balance }) => `${balance} ECO`,
+    walletCnfrs: "Cnfrs",
+    walletConfirm: "Confirmer",
+    walletDescription: "Gérez vos actifs numériques, y compris l'envoi et la réception d'ECOin, consultez votre solde et votre historique de transactions.",
+    walletDate: "Date",
+    walletFee: "Frais (Plus les frais sont élevés, plus votre transaction sera traitée rapidement)",
+    walletFeeLine: ({ fee }) => `Frais : ECO ${fee}`,
+    walletHistory: "Historique",
+    walletReceive: "Recevoir",
+    walletReset: "Réinitialiser",
+    walletSend: "Envoyer",
+    walletStatus: "Statut",
+    walletDisconnected: "Portefeuille ECOin déconnecté. Vérifiez la configuration de votre portefeuille ou l'état de la connexion.",
+    walletSentToLine: ({ destination, amount }) => `Envoyé ECO ${amount} à ${destination}`,
+    walletSettingsTitle: "Portefeuille",
+    walletSettingsDescription: "Intégrez Oasis à votre portefeuille ECOin.",
+    walletStatusMessages: {
+        invalid_amount: "Montant invalide",
+        invalid_dest: "Adresse de destination invalide",
+        invalid_fee: "Frais invalides",
+        validation_errors: "Erreurs de validation",
+        send_tx_success: "Transaction réussie",
+    },
+    walletTitle: "Portefeuille",
+    walletTotalCostLine: ({ totalCost }) => `Coût total : ECO ${totalCost}`,
+    walletTransactionId: "ID de transaction",
+    walletTxId: "ID Tx",
+    walletType: "Type",
+    walletUser: "Nom d'utilisateur",
+    walletPass: "Mot de passe",
+    walletConfiguration: "Configurer le portefeuille",
+    //cipher
+    cipher: "Chiffrement",
+    randomPassword: "Mot de passe aléatoire",
+    password: "Mot de passe",
+    text: "Texte",
+    encryptedText: "Texte chiffré",
+    iv: "Vecteur d'initialisation (IV)",
+    encryptTitle: "Chiffrez votre texte",
+    encryptDescription: "Entrez le texte que vous souhaitez chiffrer et fournissez un mot de passe.",
+    encryptButton: "Chiffrer",
+    decryptTitle: "Déchiffrez votre texte",
+    decryptDescription: "Entrez le texte chiffré et fournissez le même mot de passe utilisé pour le chiffrer.",
+    decryptButton: "Déchiffrer",
+    passwordLengthError: "Le mot de passe doit contenir au moins 32 caractères.",
+    missingFieldsError: "Texte, mot de passe ou IV non fournis.",
+    encryptionError: "Erreur lors du chiffrement du texte.",
+    decryptionError: "Erreur lors du déchiffrement du texte.",
+    //cipher
+    cipherTitle: "Chiffrement",
+    cipherDescription: "Chiffrez et déchiffrez du contenu de manière symétrique (en utilisant un mot de passe partagé).",
+    randomPassword: "Mot de passe aléatoire",
+    cipherEncryptTitle: "Chiffrer un texte",
+    cipherEncryptDescription: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre texte",
+    cipherTextLabel: "Texte à chiffrer",
+    cipherTextPlaceholder: "Entrez le texte à chiffrer...",
+    cipherPasswordLabel: "Mot de passe",
+    cipherPasswordPlaceholder: "Entrez un mot de passe...",
+    cipherEncryptButton: "Chiffrer",
+    cipherDecryptTitle: "Déchiffrer un texte",
+    cipherDecryptDescription: "Entrez le texte chiffré, le mot de passe et l'IV pour le déchiffrer.",
+    cipherEncryptedTextLabel: "Texte chiffré",
+    cipherEncryptedTextPlaceholder: "Entrez le texte chiffré...",
+    cipherIvLabel: "IV",
+    cipherIvPlaceholder: "Entrez le vecteur d'initialisation...",
+    cipherDecryptButton: "Déchiffrer"
+    }
+};

+ 2 - 2
src/ssb/cli-cmd-aliases.js

@@ -6,5 +6,5 @@ module.exports = {
   pub: 'getPublicKey',
   log: 'createLogStream',
   logt: 'messagesByType',
-  conf: 'config'
-}
+  conf: 'config',
+};

+ 11 - 57
src/ssb/index.js

@@ -1,21 +1,14 @@
 "use strict";
 
-// This module exports a function that connects to SSB and returns an interface
-// to call methods over MuxRPC. It's a thin wrapper around SSB-Client, which is
-// a thin wrapper around the MuxRPC module.
-
-const ssbClient = require("ssb-client");
-const ssbConfig = require("ssb-config");
-const ssbKeys = require("ssb-keys");
-const debug = require("debug")("oasis");
-const path = require("path");
-const lodash = require("lodash");
-const fs = require("fs");
-const os = require("os");
-
-const flotilla = require("./flotilla");
-
-// Use temporary path if we're running a test.
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const ssbClient = require(path.join(__dirname, '../server/node_modules/ssb-client'));
+const ssbConfig = require(path.join(__dirname, '../server/node_modules/ssb-config'));
+const ssbKeys = require(path.join(__dirname, '../server/node_modules/ssb-keys'));
+const debug = require('../server/node_modules/debug')('oasis');
+const lodash = require('../server/node_modules/lodash');
+
 if (process.env.OASIS_TEST) {
   ssbConfig.path = fs.mkdtempSync(path.join(os.tmpdir(), "oasis-"));
   ssbConfig.keys = ssbKeys.generate();
@@ -25,10 +18,6 @@ const socketPath = path.join(ssbConfig.path, "socket");
 const publicInteger = ssbConfig.keys.public.replace(".ed25519", "");
 const remote = `unix:${socketPath}~noauth:${publicInteger}`;
 
-/**
- * @param formatter {string} input
- * @param args {any[]} input
- */
 const log = (formatter, ...args) => {
   const isDebugEnabled = debug.enabled;
   debug.enabled = true;
@@ -36,10 +25,6 @@ const log = (formatter, ...args) => {
   debug.enabled = isDebugEnabled;
 };
 
-/**
- * @param [options] {object} - options to pass to SSB-Client
- * @returns Promise
- */
 const connect = (options) =>
   new Promise((resolve, reject) => {
     const onSuccess = (ssb) => {
@@ -54,11 +39,6 @@ let closing = false;
 let serverHandle;
 let clientHandle;
 
-/**
- * Attempts connection over Unix socket, falling back to TCP socket if that
- * fails. If the TCP socket fails, the promise is rejected.
- * @returns Promise
- */
 const attemptConnection = () =>
   new Promise((resolve, reject) => {
     const originalConnect = process.env.OASIS_TEST
@@ -70,7 +50,6 @@ const attemptConnection = () =>
       : connect({ remote });
     originalConnect
       .then((ssb) => {
-        debug("Connected to existing Scuttlebutt service over Unix socket");
         resolve(ssb);
       })
       .catch((e) => {
@@ -81,7 +60,6 @@ const attemptConnection = () =>
         }
         connect()
           .then((ssb) => {
-            log("Connected to existing Scuttlebutt service over TCP socket");
             resolve(ssb);
           })
           .catch((e) => {
@@ -105,14 +83,6 @@ const ensureConnection = (customConfig) => {
         .then((ssb) => {
           resolve(ssb);
         })
-        .catch(() => {
-          serverHandle = flotilla(customConfig);
-            attemptConnection()
-              .then(resolve)
-              .catch((e) => {
-                throw new Error(e);
-              });
-          }, 100);
         });
     });
 
@@ -128,34 +98,20 @@ module.exports = ({ offline }) => {
     log("Offline mode activated - not connecting to scuttlebutt peers or pubs");
   }
 
-  // Make a copy of `ssbConfig` to avoid mutating.
   const customConfig = JSON.parse(JSON.stringify(ssbConfig));
 
-  // Only change the config if `--offline` is true.
   if (offline === true) {
     lodash.set(customConfig, "conn.autostart", false);
   }
 
-  // Use `conn.hops`, or default to `friends.hops`, or default to `0`.
   lodash.set(
     customConfig,
     "conn.hops",
     lodash.get(ssbConfig, "conn.hops", lodash.get(ssbConfig.friends.hops, 0))
   );
 
-  /**
-   * This is "cooler", a tiny interface for opening or reusing an instance of
-   * SSB-Client.
-   */
   const cooler = {
     open() {
-      // This has interesting behavior that may be unexpected.
-      //
-      // If `clientHandle` is already an active [non-closed] connection, return that.
-      //
-      // If the connection is closed, we need to restart it. It's important to
-      // note that if we're depending on an external service (like Patchwork) and
-      // that app is closed, then Oasis will seamlessly start its own SSB service.
       return new Promise((resolve, reject) => {
         if (clientHandle && clientHandle.closed === false) {
           resolve(clientHandle);
@@ -177,13 +133,11 @@ module.exports = ({ offline }) => {
       if (clientHandle && clientHandle.closed === false) {
         clientHandle.close();
       }
-      if (serverHandle) {
-        serverHandle.close();
-      }
     },
   };
 
   cooler.open();
 
   return cooler;
-};
+};
+

+ 11 - 32
src/http/index.js

@@ -1,32 +1,23 @@
-const Koa = require("koa");
-const koaStatic = require("koa-static");
+const path = require("path");
+const Koa = require(path.join(__dirname, "../server/node_modules/koa"));
+const koaStatic = require(path.join(__dirname, "../server/node_modules/koa-static"));
 const { join } = require("path");
-const mount = require("koa-mount");
+const mount = require(path.join(__dirname, "../server/node_modules/koa-mount"));
 
-/**
- * @type function
- * @param {{ host: string, port: number, middleware: any[], allowHost: string | null }} input
- * @return function
- */
 module.exports = ({ host, port, middleware, allowHost }) => {
-  const assets = new Koa();
-  assets.use(koaStatic(join(__dirname, "..", "assets")));
-
+  const assets = new Koa()
+  assets.use(koaStatic(join(__dirname, "..", "client", "assets")));
+  
   const app = new Koa();
-
   const validHosts = [];
 
-  // All non-GET requests must have a path that doesn't start with `/blob/`.
   const isValidRequest = (request) => {
-    // All requests must use our hostname to prevent DNS rebind attacks.
     if (validHosts.includes(request.hostname) !== true) {
       console.log(`Invalid HTTP hostname: ${request.hostname}`);
       return false;
     }
 
-    // All non-GET requests must ...
     if (request.method !== "GET") {
-      // ...have a referer...
       if (request.header.referer == null) {
         console.log("No referer");
         return false;
@@ -34,13 +25,11 @@ module.exports = ({ host, port, middleware, allowHost }) => {
 
       try {
         const refererUrl = new URL(request.header.referer);
-        // ...with a valid hostname...
         if (validHosts.includes(refererUrl.hostname) !== true) {
           console.log(`Invalid referer hostname: ${refererUrl.hostname}`);
           return false;
         }
 
-        // ...and must not originate from a blob path.
         if (refererUrl.pathname.startsWith("/blob/")) {
           console.log(`Invalid referer path: ${refererUrl.pathname}`);
           return false;
@@ -51,15 +40,12 @@ module.exports = ({ host, port, middleware, allowHost }) => {
       }
     }
 
-    // If all of the above checks pass, this is a valid request.
     return true;
   };
 
   app.on("error", (err, ctx) => {
-    // Output full error objects
     console.error(err);
 
-    // Avoid printing errors for invalid requests.
     if (isValidRequest(ctx.request)) {
       err.message = err.stack;
       err.expose = true;
@@ -70,8 +56,10 @@ module.exports = ({ host, port, middleware, allowHost }) => {
 
   app.use(mount("/assets", assets));
 
-  // headers
   app.use(async (ctx, next) => {
+  
+    //console.log("Requesting:", ctx.path); // uncomment to check for HTTP requests
+    
     const csp = [
       "default-src 'none'",
       "img-src 'self'",
@@ -80,25 +68,16 @@ module.exports = ({ host, port, middleware, allowHost }) => {
       "style-src 'self'",
     ].join("; ");
 
-    // Disallow scripts.
     ctx.set("Content-Security-Policy", csp);
-
-    // Disallow <iframe> embeds from other domains.
     ctx.set("X-Frame-Options", "SAMEORIGIN");
 
     const isBlobPath = ctx.path.startsWith("/blob/");
 
     if (isBlobPath === false) {
-      // Disallow browsers overwriting declared media types.
-      //
-      // This should only happen on non-blob URLs.
       ctx.set("X-Content-Type-Options", "nosniff");
     }
 
-    // Disallow sharing referrer with other domains.
     ctx.set("Referrer-Policy", "same-origin");
-
-    // Disallow extra browser features except audio output.
     ctx.set("Feature-Policy", "speaker 'self'");
 
     const validHostsString = validHosts.join(" or ");
@@ -120,7 +99,6 @@ module.exports = ({ host, port, middleware, allowHost }) => {
     const address = server.address();
 
     if (typeof address === "string") {
-      // This shouldn't happen, but TypeScript was complaining about it.
       throw new Error("HTTP server should never bind to Unix socket");
     }
 
@@ -137,3 +115,4 @@ module.exports = ({ host, port, middleware, allowHost }) => {
 
   return server;
 };
+

+ 6 - 8
src/cli.js

@@ -1,16 +1,13 @@
 "use strict";
 
-const yargs = require("yargs");
-const { hideBin } = require("yargs/helpers"); // Helper to parse arguments
-const _ = require("lodash");
+const path = require('path');
+const yargs = require(path.join(__dirname, '../server/node_modules/yargs'));
+const { hideBin } = require(path.join(__dirname, '../server/node_modules/yargs/helpers'));
+const _ = require(path.join(__dirname, '../server/node_modules/lodash'));
 
-const moduleAlias = require('module-alias');
+const moduleAlias = require(path.join(__dirname, '../server/node_modules/module-alias'));
 moduleAlias.addAlias('punycode', 'punycode/');
 
-/**
- * @param {object} presets
- * @param {string} defaultConfigFile
- */
 const cli = (presets, defaultConfigFile) =>
   yargs(hideBin(process.argv))
     .scriptName("oasis")
@@ -85,3 +82,4 @@ const cli = (presets, defaultConfigFile) =>
     .epilog(`The defaults can be configured in ${defaultConfigFile}.`).argv;
 
 module.exports = { cli };
+

+ 1 - 1
src/config.js

@@ -1,7 +1,7 @@
 const fs = require('fs');
 const path = require('path');
 
-const configFilePath = path.join(__dirname, 'config.json');
+const configFilePath = path.join(__dirname, 'oasis-config.json');
 
 if (!fs.existsSync(configFilePath)) {
   const defaultConfig = {

+ 4 - 1
src/config.json

@@ -1,4 +1,7 @@
 {
+  "themes": {
+    "current": "Dark-SNH"
+  },
   "modules": {
     "popularMod": "on",
     "topicsMod": "on",
@@ -18,4 +21,4 @@
     "pass": "ecoinrpc",
     "fee": "0.01"
   }
-}
+}

+ 72 - 0
src/configs/server-config.json

@@ -0,0 +1,72 @@
+{
+  "replicationScheduler": {
+    "autostart": true,
+    "partialReplication": null
+  },
+  "pub": true,
+  "local": true,
+  "friends": {
+    "dunbar": 300,
+    "hops": 2
+  },
+  "gossip": {
+    "connections": 20,
+    "local": true,
+    "friends": true,
+    "seed": true,
+    "global": true
+  },
+  "connections": {
+    "incoming": {
+      "net": [
+        {
+          "scope": "public",
+          "transform": "shs",
+          "port": 8008
+        },
+        {
+          "scope": "device",
+          "transform": "shs",
+          "port": 8008
+        }
+      ],
+      "tunnel": [
+        {
+          "scope": "public",
+          "portal": "@1wOEiCjJJ0nEs1OABUIV20valZ1LHUsfHJY/ivBoM8Y=.ed25519",
+          "transform": "shs"
+        }
+      ],
+      "onion": [
+        {
+          "scope": "public",
+          "transform": "shs"
+        }
+      ],
+      "ws": [
+        {
+          "scope": "public",
+          "transform": "shs"
+        }
+      ]
+    },
+    "outgoing": {
+      "net": [
+        {
+          "transform": "shs"
+        }
+      ],
+      "ws": [
+        {
+          "transform": "shs"
+        }
+      ],
+      "tunnel": [
+        {
+          "transform": "shs"
+        }
+      ]
+    }
+  }
+}
+

+ 182 - 285
src/models.js

@@ -1,19 +1,20 @@
 "use strict";
 
-const debug = require("debug")("oasis");
-const { isRoot, isReply: isComment } = require("ssb-thread-schema");
-const lodash = require("lodash");
-const prettyMs = require("pretty-ms");
-const pullAbortable = require("pull-abortable");
-const pullParallelMap = require("pull-paramap");
-const pull = require("pull-stream");
-const pullSort = require("pull-sort");
-const ssbRef = require("ssb-ref");
+const debug = require("../server/node_modules/debug")("oasis");
+const { isRoot, isReply: isComment } = require("../server/node_modules/ssb-thread-schema");
+const lodash = require("../server/node_modules/lodash");
+const prettyMs = require("../server/node_modules/pretty-ms");
+const pullAbortable = require("../server/node_modules/pull-abortable");
+const pullParallelMap = require("../server/node_modules/pull-paramap");
+const pull = require("../server/node_modules/pull-stream");
+const pullSort = require("../server/node_modules/pull-sort");
+
+const ssbRef = require("../server/node_modules/ssb-ref");
 
 const {
   RequestManager,
   HTTPTransport,
-  Client } = require("@open-rpc/client-js");
+  Client } = require("../server/node_modules/@open-rpc/client-js");
 
 const isEncrypted = (message) => typeof message.value.content === "string";
 const isNotEncrypted = (message) => isEncrypted(message) === false;
@@ -45,8 +46,7 @@ const isBlogPost = (message) =>
 
 const isTextLike = (message) => isPost(message) || isBlogPost(message);
 
-// HACK: https://github.com/ssbc/ssb-thread-schema/issues/4
-const isSubtopic = require("ssb-thread-schema/post/nested-reply/validator");
+const isSubtopic = require("../server/node_modules/ssb-thread-schema/post/nested-reply/validator");
 
 const nullImage = `&${"0".repeat(43)}=.sha256`;
 
@@ -58,7 +58,6 @@ const defaultOptions = {
 
 const publicOnlyFilter = pull.filter(isNotPrivate);
 
-/** @param {object[]} customOptions */
 const configure = (...customOptions) =>
   Object.assign({}, defaultOptions, ...customOptions);
 
@@ -100,27 +99,20 @@ module.exports = ({ cooler, isPublic }) => {
       )
     );
   };
-  // TODO: an alternative would be using ssb.names if available and just loading this as a fallback
   const feeds_to_name = {};
   let all_the_names = {};
-
-  let dirty = false; // just stop mindless work (nothing changed) could be smarter thou
-  let running = false; // don't run twice
-
-  // transposeLookupTable flips the lookup around (form feed->name to name->feed)
-  // and also enhances the entries with image and relationship info
+  let dirty = false;
+  let running = false;
   const transposeLookupTable = () => {
     if (!dirty) return;
     if (running) return;
     running = true;
 
-    // invalidate old cache
-    // regenerate a new thing because we don't know which entries will be gone
     all_the_names = {};
 
     const allFeeds = Object.keys(feeds_to_name);
-    console.log(`updating ${allFeeds.length} feeds`);
-    console.time("transpose-name-index");
+    console.log(`Synced-feeds: [ ${allFeeds.length} ]`);
+    console.time("Sync-time");
 
     const lookups = [];
     for (const feed of allFeeds) {
@@ -128,21 +120,17 @@ module.exports = ({ cooler, isPublic }) => {
       let pair = { feed, name: e.name };
       lookups.push(enhanceFeedInfo(pair));
     }
-
-    // wait for all image and follow lookups
     Promise.all(lookups)
       .then(() => {
-        dirty = false; // all updated
+        dirty = false; 
         running = false;
-        console.timeEnd("transpose-name-index");
+        console.timeEnd("Sync-time");
       })
       .catch((err) => {
         running = false;
-        console.warn("lookup transposition failed:", err);
+        console.warn("lookup sync failed:", err);
       });
   };
-
-  // this function adds the avatar image and relationship to the all_the_names lookup table
   const enhanceFeedInfo = ({ feed, name }) => {
     return new Promise((resolve, reject) => {
       getAbout({ feedId: feed, key: "image" })
@@ -155,19 +143,16 @@ module.exports = ({ cooler, isPublic }) => {
           ) {
             img = img.link;
           } else if (img === null) {
-            img = nullImage; // default empty image if we don't have one
+            img = nullImage; 
           }
 
           models.friend
             .getRelationship(feed)
             .then((rel) => {
-              // append and update lookup table
               let feeds_named = all_the_names[name] || [];
               feeds_named.push({ feed, name, rel, img });
               all_the_names[name.toLowerCase()] = feeds_named;
               resolve();
-
-              // TODO: append if these fail!?
             })
             .catch(reject);
         })
@@ -175,135 +160,126 @@ module.exports = ({ cooler, isPublic }) => {
     });
   };
 
-  models.about = {
-    publicWebHosting: async (feedId) => {
-      const result = await getAbout({
-        key: "publicWebHosting",
+models.about = {
+  publicWebHosting: async (feedId) => {
+    const result = await getAbout({
+      key: "publicWebHosting",
+      feedId,
+    });
+    return result === true;
+  },
+  name: async (feedId) => {
+    if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
+      return "Redacted";
+    }
+    return (
+      (await getAbout({
+        key: "name",
         feedId,
-      });
-      return result === true;
-    },
-    name: async (feedId) => {
-      if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
-        return "Redacted";
-      }
-
-      // TODO: could possibly use all_the_names
-      return (
-        (await getAbout({
-          key: "name",
-          feedId,
-        })) || feedId.slice(1, 1 + 8)
-      ); // First 8 chars of public key
-    },
-    named: (name) => {
-      let found = [];
-      let matched = Object.keys(all_the_names).filter((n) => {
-        return n.startsWith(name.toLowerCase());
-      });
-      for (const m of matched) {
-        found = found.concat(all_the_names[m]);
-      }
-      return found;
-    },
-    image: async (feedId) => {
-      if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
-        return nullImage;
-      }
+      })) || feedId.slice(1, 1 + 8)
+    );
+  },
+  named: (name) => {
+    let found = [];
+    let matched = Object.keys(all_the_names).filter((n) => {
+      return n.startsWith(name.toLowerCase());
+    });
+    for (const m of matched) {
+      found = found.concat(all_the_names[m]);
+    }
+    return found;
+  },
+  image: async (feedId) => {
+    if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
+      return nullImage;
+    }
 
-      const raw = await getAbout({
-        key: "image",
-        feedId,
-      });
+    const raw = await getAbout({
+      key: "image",
+      feedId,
+    });
 
-      if (raw == null || raw.link == null) {
-        return nullImage;
-      }
+    if (raw == null || raw.link == null) {
+      return nullImage;
+    }
 
-      if (typeof raw.link === "string") {
-        return raw.link;
-      }
-      return raw;
-    },
-    description: async (feedId) => {
-      if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
-        return "Redacted";
-      }
+    if (typeof raw.link === "string") {
+      return raw.link;
+    }
+    return raw;
+  },
+  description: async (feedId) => {
+    if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
+      return "Redacted";
+    }
 
-      const raw =
-        (await getAbout({
-          key: "description",
-          feedId,
-        })) || "";
-      return raw;
-    },
-    // This needs to run in the background but also needs to be aborted
-    // in index.js when the server closes. There's also an interval that
-    // needs to be cleared. TODO: Ensure that there's never more than
-    // one interval running at a time.
-    _startNameWarmup() {
-      const abortable = pullAbortable();
-      let intervals = [];
-      cooler.open().then((ssb) => {
-        console.time("about-name-warmup"); // benchmark the time it takes to stream all existing about messages
-        pull(
-          ssb.query.read({
-            live: true, // keep streaming new messages as they arrive
-            query: [
-              {
-                $filter: {
-                  // all messages of type:about that have a name field that is typeof string
-                  value: {
-                    content: {
-                      type: "about",
-                      name: { $is: "string" },
-                    },
+    const raw =
+      (await getAbout({
+        key: "description",
+        feedId,
+      })) || "";
+    return raw;
+  },
+  _startNameWarmup() {
+    const abortable = pullAbortable();
+    let intervals = [];
+    cooler.open().then((ssb) => {
+      console.time("Warmup-time");
+      pull(
+        ssb.query.read({
+          live: true,
+          query: [
+            {
+              $filter: {
+                value: {
+                  content: {
+                    type: "about",
+                    name: { $is: "string" },
                   },
                 },
               },
-            ],
-          }),
-          abortable,
-          pull.filter((msg) => {
-            // backlog of data is done, only new values from now on
-            if (msg.sync && msg.sync === true) {
-              console.timeEnd("about-name-warmup");
-              transposeLookupTable(); // fire once now
-              intervals.push(setInterval(transposeLookupTable, 1000 * 60)); // and then every 60 seconds
-              return false;
-            }
-            // only pick messages about self
-            return msg.value.author == msg.value.content.about;
-          }),
-          pull.drain((msg) => {
-            const name = msg.value.content.name;
-            const ts = msg.value.timestamp;
-            const feed = msg.value.author;
-
-            const newEntry = { name, ts };
-            const currentEntry = feeds_to_name[feed];
-            if (typeof currentEntry == "undefined") {
-              dirty = true;
-              feeds_to_name[feed] = newEntry;
-            } else if (currentEntry.ts < ts) {
-              // overwrite entry if it's newer
-              dirty = true;
-              feeds_to_name[feed] = newEntry;
-            }
-          }, (err) => {
-            console.error(err);
-          })
-        );
-      });
+            },
+          ],
+        }),
+        abortable,
+        pull.filter((msg) => {
+          if (msg.sync && msg.sync === true) {
+            console.timeEnd("Warmup-time");
+            transposeLookupTable();
+            intervals.push(setInterval(transposeLookupTable, 1000 * 60)); 
+            return false;
+          }
+          return msg.value.author == msg.value.content.about;
+        }),
+        pull.drain((msg) => {
+          const name = msg.value.content.name;
+          const ts = msg.value.timestamp;
+          const feed = msg.value.author;
+
+          const newEntry = { name, ts };
+          const currentEntry = feeds_to_name[feed];
+          if (typeof currentEntry == "undefined") {
+            dirty = true;
+            feeds_to_name[feed] = newEntry;
+          } else if (currentEntry.ts < ts) {
+            dirty = true;
+            feeds_to_name[feed] = newEntry;
+          }
+        }, (err) => {
+          console.error(err);
+        })
+      );
+    });
+
+    return {
+      close: () => {
+        abortable.abort();
+        intervals.forEach((i) => clearInterval(i));
+      },
+    };
+  },
+};
 
-      return {
-        close: () => {
-          abortable.abort();
-          intervals.forEach((i) => clearInterval(i));
-        },
-      };
-    },
-  };
 
   models.blob = {
     get: async ({ blobId }) => {
@@ -334,7 +310,6 @@ module.exports = ({ cooler, isPublic }) => {
       cooler
         .open()
         .then((ssb) => {
-          // This does not wait for the blob.
           ssb.blobs.want(blobId);
         })
         .catch((err) => {
@@ -356,7 +331,6 @@ module.exports = ({ cooler, isPublic }) => {
   };
 
   models.friend = {
-    /** @param {{ feedId: string, following: boolean, blocking: boolean }} input */
     setRelationship: async ({ feedId, following, blocking }) => {
       if (following && blocking) {
         throw new Error("Cannot follow and block at the same time");
@@ -367,7 +341,6 @@ module.exports = ({ cooler, isPublic }) => {
         current.following === following && current.blocking === blocking;
 
       if (alreadySet) {
-        // The following state is already set, don't re-set it.
         return;
       }
 
@@ -379,7 +352,7 @@ module.exports = ({ cooler, isPublic }) => {
         following,
         blocking,
       };
-      transposeLookupTable(); // invalidate @mentions table
+      transposeLookupTable(); 
       return ssb.publish(content);
     },
     follow: (feedId) =>
@@ -406,10 +379,6 @@ module.exports = ({ cooler, isPublic }) => {
         blocking: false,
         following: false,
       }),
-    /**
-     * @param feedId {string}
-     * @returns {Promise<{me: boolean, following: boolean, blocking: boolean, followsMe: boolean }>}
-     */
     getRelationship: async (feedId) => {
       const ssb = await cooler.open();
       const { id } = ssb;
@@ -472,7 +441,6 @@ module.exports = ({ cooler, isPublic }) => {
       return new Promise((resolve, reject) => {
         pull(
           peersSource,
-          // https://github.com/staltz/ssb-conn/issues/9
           pull.take(1),
           pull.collect((err, val) => {
             if (err) return reject(err);
@@ -499,7 +467,6 @@ module.exports = ({ cooler, isPublic }) => {
         const expectedName = "TypeError";
         const expectedMessage = "Cannot read property 'close' of null";
         if (e.name === expectedName && e.message === expectedMessage) {
-          // https://github.com/staltz/ssb-lan/issues/5
           debug("ssbConn is already stopped -- caught error");
         } else {
           throw new Error(e);
@@ -522,7 +489,6 @@ module.exports = ({ cooler, isPublic }) => {
       const progress = await ssb.progress();
       let previousTarget = progress.indexes.target;
 
-      // Automatically timeout after 5 minutes.
       let keepGoing = true;
       const timeoutInterval = setTimeout(() => {
         keepGoing = false;
@@ -530,7 +496,6 @@ module.exports = ({ cooler, isPublic }) => {
 
       await ssb.conn.start();
 
-      // Promise that resolves the number of new messages after 5 seconds.
       const diff = async () =>
         new Promise((resolve) => {
           setTimeout(async () => {
@@ -545,14 +510,12 @@ module.exports = ({ cooler, isPublic }) => {
 
       debug("Starting sync, waiting for new messages...");
 
-      // Wait until we **start** receiving messages.
       while (keepGoing && (await diff()) === 0) {
         debug("Received no new messages.");
       }
 
       debug("Finished waiting for first new message.");
 
-      // Wait until we **stop** receiving messages.
       while (keepGoing && (await diff()) > 0) {
         debug(`Still receiving new messages...`);
       }
@@ -567,7 +530,6 @@ module.exports = ({ cooler, isPublic }) => {
       const ssb = await cooler.open();
       return await ssb.invite.accept(invite);
     },
-    // Returns promise, does not wait for rebuild to finish.
     rebuild: async () => {
       const ssb = await cooler.open();
       return ssb.rebuild();
@@ -630,15 +592,6 @@ module.exports = ({ cooler, isPublic }) => {
       );
     });
   };
-
-  /**
-   * Returns a function that filters messages based on who published the message.
-   *
-   * `null` means we don't care, `true` means it must be true, and `false` means
-   * that the value must be false. For example, if you set `me = true` then it
-   * will only allow messages that are from you. If you set `blocking = true`
-   * then you only see message from people you block.
-   */
   const socialFilter = async ({
     following = null,
     blocking = false,
@@ -705,8 +658,9 @@ module.exports = ({ cooler, isPublic }) => {
   }
 
   const transform = (ssb, messages, myFeedId) =>
-    Promise.all(
-      messages.map(async (msg) => {
+   Promise.all(
+    messages.map(async (msg) => {
+      try {
         debug("transforming %s", msg.key);
 
         if (msg == null) {
@@ -721,11 +675,10 @@ module.exports = ({ cooler, isPublic }) => {
 
         const referenceStream = ssb.backlinks.read({
           query: [filterQuery],
-          index: "DTA", // use asserted timestamps
+          index: "DTA",
           private: true,
           meta: true,
         });
-
         if (lodash.get(msg, "value.content.type") === "blog") {
           const blogTitle = msg.value.content.title;
           const blogSummary = lodash.get(msg, "value.content.summary", null);
@@ -737,7 +690,6 @@ module.exports = ({ cooler, isPublic }) => {
           }
           lodash.set(msg, "value.content.text", textElements.join("\n\n"));
         }
-
         const rawVotes = await new Promise((resolve, reject) => {
           pull(
             referenceStream,
@@ -759,28 +711,20 @@ module.exports = ({ cooler, isPublic }) => {
             })
           );
         });
-
-        // { @key: 1, @key2: 0, @key3: 1 }
-        //
-        // only one vote per person!
         const reducedVotes = rawVotes.reduce((acc, vote) => {
           acc[vote.value.author] = vote.value.content.vote.value;
           return acc;
         }, {});
 
-        // gets *only* the people who voted 1
-        // [ @key, @key, @key ]
         const voters = Object.entries(reducedVotes)
           .filter(([, value]) => value === 1)
           .map(([key]) => key);
 
-        // get an array of voter names, for display on hovers
         const pendingVoterNames = voters.map(async (author) => ({
           name: await models.about.name(author),
           key: author,
         }));
         const voterNames = await Promise.all(pendingVoterNames);
-
         const { name, avatarId, avatarUrl } = await getUserInfo(
           msg.value.author
         );
@@ -801,22 +745,12 @@ module.exports = ({ cooler, isPublic }) => {
             }
           }
         }
-
-        const channel = lodash.get(msg, "value.content.channel");
-        const hasChannel = typeof channel === "string" && channel.length > 2;
-
-        if (hasChannel && hasNoRoot(msg)) {
-          msg.value.content.text += `\n\n#${channel}`;
-        }
-
         const ts = new Date(msg.value.timestamp);
         let isoTs;
 
         try {
           isoTs = ts.toISOString();
         } catch (e) {
-          // Just in case it's an invalid date. :(
-          debug(e);
           const receivedTs = new Date(msg.timestamp);
           isoTs = receivedTs.toISOString();
         }
@@ -831,7 +765,6 @@ module.exports = ({ cooler, isPublic }) => {
           id: avatarId,
           url: avatarUrl,
         });
-
         if (isTextLike(msg) && hasNoRoot(msg) && hasNoFork(msg)) {
           lodash.set(msg, "value.meta.postType", "post");
         } else if (isTextLike(msg) && hasRoot(msg) && hasNoFork(msg)) {
@@ -859,8 +792,12 @@ module.exports = ({ cooler, isPublic }) => {
         lodash.set(msg, "value.meta.blocking", blocking);
 
         return msg;
-      })
-    );
+
+      } catch (err) {
+        return null; 
+      }
+    })
+  );
 
   const getLimitPost = async (feedId, reverse) => {
     const ssb = await cooler.open();
@@ -900,10 +837,6 @@ module.exports = ({ cooler, isPublic }) => {
       defaultOptions.reverse = !(gt >= 0 && lt < 0);
       const options = configure(defaultOptions, customOptions);
       const { blocking } = await models.friend.getRelationship(feedId);
-
-      // Avoid streaming any messages from this feed. If we used the social
-      // filter here it would continue streaming all messages from this author
-      // until it consumed the entire feed.
       if (blocking) {
         return [];
       }
@@ -928,53 +861,50 @@ module.exports = ({ cooler, isPublic }) => {
       if (!defaultOptions.reverse) return messages.reverse();
       else return messages;
     },
-    mentionsMe: async (customOptions = {}) => {
-      const ssb = await cooler.open();
-
-      const myFeedId = ssb.id;
+  mentionsMe: async (customOptions = {}) => {
+    const ssb = await cooler.open();
 
-      const query = [
-        {
-          $filter: {
-            dest: myFeedId,
-          },
-        },
-      ];
+    const myFeedId = ssb.id;
 
-      const messages = await getMessages({
-        myFeedId,
-        customOptions,
-        ssb,
-        query,
-        filter: (msg) =>
-          msg.value.author !== myFeedId &&
-          lodash.get(msg, "value.meta.private") !== true,
-      });
-
-      return messages;
+    const query = [
+    {
+      $filter: {
+        dest: myFeedId,
+      },
     },
-    fromHashtag: async (hashtag, customOptions = {}) => {
-      const ssb = await cooler.open();
+    ];
 
-      const myFeedId = ssb.id;
+  const messages = await getMessages({
+    myFeedId,
+    customOptions,
+    ssb,
+    query,
+    filter: (msg) =>
+      lodash.get(msg, "value.meta.private") !== true,
+  });
+  return messages;
+  },
 
-      const query = [
-        {
-          $filter: {
-            dest: `#${hashtag}`,
-          },
-        },
-      ];
+  fromHashtag: async (hashtag, customOptions = {}) => {
+   const ssb = await cooler.open();
+   const myFeedId = ssb.id;
+   const query = [
+    {
+      $filter: {
+        dest: `#${hashtag}`,
+      },
+    },
+  ];
+  const messages = await getMessages({
+    myFeedId,
+    customOptions,
+    ssb,
+    query,
+   });
 
-      const messages = await getMessages({
-        myFeedId,
-        customOptions,
-        ssb,
-        query,
-      });
+   return messages;
+  },
 
-      return messages;
-    },
     topicComments: async (rootId, customOptions = {}) => {
       const ssb = await cooler.open();
 
@@ -1372,8 +1302,6 @@ module.exports = ({ cooler, isPublic }) => {
               if (acc[author] == null) {
                 acc[author] = {};
               }
-
-              // Only accept values between -1 and 1
               acc[author][target] = Math.max(-1, Math.min(1, value));
 
               return acc;
@@ -1383,20 +1311,11 @@ module.exports = ({ cooler, isPublic }) => {
               if (err) {
                 return reject(err);
               }
-
-              // HACK: Can we do this without a reduce()? I think this makes the
-              // stream much slower than it needs to be. Also, we should probably
-              // be indexing these rather than building the stream on refresh.
-
               const adjustedObj = Object.entries(obj).reduce(
                 (acc, [author, values]) => {
                   if (author === myFeedId) {
                     return acc;
                   }
-
-                  // The value of a users vote is 1 / (1 + total votes), the
-                  // more a user votes, the less weight is given to each vote.
-
                   const entries = Object.entries(values);
                   const total = 1 + Math.log(entries.length);
 
@@ -1427,7 +1346,6 @@ module.exports = ({ cooler, isPublic }) => {
                     cb(null, null);
                   }
                 }),
-                // avoid private messages (!) and non-posts
                 pull.filter(
                   (message) =>
                     message &&
@@ -1474,13 +1392,10 @@ module.exports = ({ cooler, isPublic }) => {
                 debug("getting root ancestor of %s", msg.key);
 
                 if (isEncrypted(msg)) {
-                  // Private message we can't decrypt, stop looking for parents.
                   debug("private message");
                   if (parents.length > 0) {
-                    // If we already have some parents, return those.
                     resolve(parents);
                   } else {
-                    // If we don't know of any parents, resolve this message.
                     resolve(msg);
                   }
                 } else if (msg.value.content.type !== "post") {
@@ -1492,7 +1407,6 @@ module.exports = ({ cooler, isPublic }) => {
                 ) {
                   debug("subtopic, get the parent");
                   try {
-                    // It's a subtopic, get the parent!
                     ssb
                       .get({
                         id: msg.value.content.fork,
@@ -1513,7 +1427,6 @@ module.exports = ({ cooler, isPublic }) => {
                 ) {
                   debug("comment: %s", msg.value.content.root);
                   try {
-                    // It's a thread subtopic, get the parent!
                     ssb
                       .get({
                         id: msg.value.content.root,
@@ -1532,8 +1445,6 @@ module.exports = ({ cooler, isPublic }) => {
                   debug("got root ancestor");
                   resolve(msg);
                 } else {
-                  // type !== "post", probably
-                  // this should show up as JSON
                   debug(
                     "got mysterious root ancestor that fails all known schemas"
                   );
@@ -1553,7 +1464,7 @@ module.exports = ({ cooler, isPublic }) => {
 
               const referenceStream = ssb.backlinks.read({
                 query: [filterQuery],
-                index: "DTA", // use asserted timestamps
+                index: "DTA",
               });
               pull(
                 referenceStream,
@@ -1566,13 +1477,10 @@ module.exports = ({ cooler, isPublic }) => {
                   const fork = lodash.get(msg, "value.content.fork");
 
                   if (root !== key && fork !== key) {
-                    // mention
                     return false;
                   }
 
                   if (fork === key) {
-                    // not a subtopic of this post
-                    // it's a subtopic **of a subtopic** of this post
                     return false;
                   }
 
@@ -1587,8 +1495,6 @@ module.exports = ({ cooler, isPublic }) => {
                 })
               );
             });
-
-          // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
           const flattenDeep = (arr1) =>
             arr1.reduce(
               (acc, val) =>
@@ -1675,7 +1581,7 @@ module.exports = ({ cooler, isPublic }) => {
     publishProfileEdit: async ({ name, description, image }) => {
       const ssb = await cooler.open();
       if (image.length > 0) {
-        // 25 MiB check
+        // 25 MiB check (here we set max file size allowed!)
         const megabyte = Math.pow(2, 20);
         const maxSize = 25 * megabyte;
         if (image.length > maxSize) {
@@ -1731,15 +1637,10 @@ module.exports = ({ cooler, isPublic }) => {
 
       if (isRoot(message) !== true) {
         const messageString = JSON.stringify(message, null, 2);
-        throw new Error(`message should be valid root post: ${messageString}`);
       }
-
       return post.publish(message);
     },
     comment: async ({ parent, message }) => {
-      // Set `root` to `parent`'s root.
-      // If `parent` doesn't have a root, use the parent's key.
-      // If `parent` has a fork, you must use the parent's key.
       const parentKey = parent.key;
       const parentFork = lodash.get(parent, "value.content.fork");
       const parentRoot = lodash.get(parent, "value.content.root", parentKey);
@@ -1753,9 +1654,6 @@ module.exports = ({ cooler, isPublic }) => {
               typeof recipient.link === "string" &&
               recipient.link.length
             ) {
-              // Some interfaces, like Patchbay, put `{ name, link }` objects in
-              // `recps`. The comment schema says this is invalid, so we want to
-              // fix the `recps` before publishing.
               return recipient.link;
             } else {
               return recipient;
@@ -1771,7 +1669,7 @@ module.exports = ({ cooler, isPublic }) => {
 
       message.root = parentHasFork ? parentKey : parentRoot;
       message.branch = await post.branch({ root: parent.key });
-      message.type = "post"; // redundant but used for validation
+      message.type = "post"; 
 
       if (isComment(message) !== true) {
         const messageString = JSON.stringify(message, null, 2);
@@ -1846,7 +1744,6 @@ module.exports = ({ cooler, isPublic }) => {
       const messages = await new Promise((resolve, reject) => {
         pull(
           source,
-          // Make sure we're only getting private messages that are posts.
           pull.filter(
             (message) =>
               isDecrypted(message) &&

File diff suppressed because it is too large
+ 0 - 174
src/server.js


+ 231 - 0
src/server/SSB_server.js

@@ -0,0 +1,231 @@
+#!/usr/bin/env node
+
+const moduleAlias = require('module-alias');
+moduleAlias.addAlias('punycode', 'punycode/');
+
+var fs = require('fs')
+var path = require('path')
+const SecretStack = require('secret-stack')
+var caps = require('ssb-caps')
+var SSB = require('ssb-db')
+var Client       = require('ssb-client')
+var cmdAliases   = require('../client/cli-cmd-aliases')
+var packageJson  = require('./package.json')
+var Config       = require('ssb-config/inject')
+var minimist     = require('minimist')
+var muxrpcli     = require('muxrpcli')
+
+const configPath = path.resolve(__dirname, '../configs', 'server-config.json');
+const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
+
+let argv = process.argv.slice(2);
+const i = argv.indexOf('--');
+const conf = argv.slice(i + 1);
+argv = ~i ? argv.slice(0, i) : argv;
+
+let config = Config('ssb', minimist(conf));
+config = {...config, ...configData};
+
+const manifestFile = path.join(config.path, 'manifest.json');
+
+console.log("=========================");
+console.log("Package:", packageJson.name, "[Version: " + packageJson.version + "]");
+//console.log("Server path:", config.path);
+//console.log("Loading configuration from:", configPath);
+console.log("Logging Level:", config.logging.level);
+console.log("Public Key ID: [", config.keys.public,"]");
+console.log("=========================");
+const modules = [
+  'ssb-master', 'ssb-gossip', 'ssb-ebt', 'ssb-friends', 'ssb-blobs', 'ssb-lan', 'ssb-meme',
+  'ssb-ooo', 'ssb-plugins', 'ssb-conn', 'ssb-box', 'ssb-search', 'ssb-friend-pub', 'ssb-invite-client',
+  'ssb-logging', 'ssb-replication-scheduler', 'ssb-partial-replication', 'ssb-about', 'ssb-onion',
+  'ssb-unix-socket', 'ssb-no-auth', 'ssb-backlinks', 'ssb-links'
+];
+console.log("Modules loaded: [", modules.length, "] ->", modules.join(', '));
+console.log("=========================");
+
+function showProgress(progress) {
+  function bar(r) {
+    let s = '\r', M = 50;
+    for (let i = 0; i < M; i++) {
+      s += i < M * r ? '*' : '.';
+    }
+    return s;
+  }
+
+  function round(n, p) {
+    return Math.round(n * p) / p;
+  }
+
+  function percent(n) {
+    return (round(n, 1000) * 100).toString().substring(0, 4) + '%';
+  }
+
+  function rate(prog) {
+    if (prog.target == prog.current) return 1;
+    return (prog.current - prog.start) / (prog.target - prog.start);
+  }
+
+  let prog = -1;
+  const int = setInterval(function () {
+    const p = progress();
+    let r = 1, c = 0;
+    const tasks = [];
+    for (let k in p) {
+      const _r = rate(p[k]);
+      if (_r < 1)
+        tasks.push(k + ':' + percent(_r));
+      r = Math.min(_r, r);
+      c++;
+    }
+    if (r != prog) {
+      prog = r;
+      const msg = tasks.join(', ');
+      process.stdout.write('\r' + bar(prog) + ' (' + msg + ')\x1b[K\r');
+    }
+  }, 333);
+  int.unref && int.unref();
+}
+
+if (argv[0] === 'start') {
+  const tribes = require('ssb-tribes');
+  const conn = require('ssb-conn');
+  const legacy_conn = require('ssb-legacy-conn');
+  const db2 = require('ssb-db2');
+  const replication_scheduler = require('ssb-replication-scheduler');
+  const friends = require('ssb-friends');
+  const ebt = require('ssb-ebt');
+  const box = require('ssb-box');
+  const threads = require('ssb-threads');
+  const invite = require('ssb-invite');
+  const conn_db = require('ssb-conn-db');
+  const search2 = require('ssb-search2');
+  const friend_pub = require('ssb-friend-pub');
+  const invite_client = require('ssb-invite-client');
+  const tunnel = require('ssb-tunnel');
+  const conn_query = require('ssb-conn-query');
+  const conn_hub = require('ssb-conn-hub');
+  const conn_staging = require('ssb-conn-staging');
+  const device_address = require('ssb-device-address');
+  const gossip = require('ssb-gossip');
+  const master = require('ssb-master');
+  const logging = require('ssb-logging');
+  const partial_replication = require('ssb-partial-replication');
+  const about = require('ssb-about');
+  const onion = require('ssb-onion');
+  const unix = require('ssb-unix-socket');
+  const auth = require('ssb-no-auth');
+  const backlinks = require('ssb-backlinks');
+  const links = require('ssb-links');
+
+  function createSsbServer() {
+    return SecretStack({ caps }).use(SSB, gossip, tribes, conn, db2, master, ebt, box, threads, invite, conn_db, search2, friend_pub, invite_client, tunnel, config, conn_query, conn_hub, conn_staging, device_address, friends, logging, replication_scheduler, partial_replication, about, onion, unix, auth, backlinks, links);
+  }
+
+  const Server = createSsbServer()
+    .use(require('ssb-master'))
+    .use(require('ssb-gossip'))
+    .use(require('ssb-ebt'))
+    .use(require('ssb-friends'))
+    .use(require('ssb-blobs'))
+    .use(require('ssb-lan'))
+    .use(require('ssb-meme'))
+    .use(require('ssb-ooo'))
+    .use(require('ssb-plugins'))
+    .use(require('ssb-conn'))
+    .use(require('ssb-box'))
+    .use(require('ssb-search'))
+    .use(require('ssb-friend-pub'))
+    .use(require('ssb-invite-client'))
+    .use(require('ssb-logging'))
+    .use(require('ssb-replication-scheduler'))
+    .use(require('ssb-partial-replication'))
+    .use(require('ssb-about'))
+    .use(require('ssb-onion'))
+    .use(require('ssb-unix-socket'))
+    .use(require('ssb-no-auth'))
+    .use(require('ssb-backlinks'))
+    .use(require("ssb-tangle"))
+    .use(require('ssb-links'))
+    .use(require('ssb-query'));
+
+  require('ssb-plugins').loadUserPlugins(Server, config);
+
+  const server = Server(config);
+
+  fs.writeFileSync(manifestFile, JSON.stringify(server.getManifest(), null, 2));
+
+  if (process.stdout.isTTY && (config.logging.level !== 'info')) {
+    showProgress(server.progress);
+  }
+
+  let manifest;
+  try {
+    manifest = JSON.parse(fs.readFileSync(manifestFile));
+  } catch (err) {
+    throw explain(err, 'no manifest file - should be generated first time server is run');
+  }
+
+  const opts = {
+    manifest: manifest,
+    port: config.port,
+    host: 'localhost',
+    caps: config.caps,
+    key: config.key || config.keys.id
+  };
+
+const levelup = require('../server/node_modules/levelup');
+const originalLevelUp = levelup.LevelUp;
+require('../server/node_modules/levelup').LevelUp = function (...args) {
+  const levelupInstance = new originalLevelUp(...args);
+  levelupInstance.on('error', function (err) {
+    if (err && err.message && err.message.includes('LOCK')) {
+      return;
+    }
+    this.emit('error', err);
+  });
+  return levelupInstance;
+};
+process.on('uncaughtException', function (err) {
+  if (err && err.message && err.message.includes('LOCK')) {
+    return;
+  }
+  throw err;
+});
+
+Client(config.keys, opts, function (err, rpc) {
+  if (err) {
+    process.exit(1);
+  }
+  for (let k in cmdAliases) {
+    rpc[k] = rpc[cmdAliases[k]];
+    manifest[k] = manifest[cmdAliases[k]];
+  }
+
+    manifest.config = 'sync';
+    rpc.config = function (cb) {
+      console.log(JSON.stringify(config, null, 2));
+      cb();
+    };   
+    function validateParams(argv, manifest, rpc, verbose) {
+        if (!Array.isArray(argv)) {
+        return false;
+    }   
+    if (typeof manifest !== 'object' || manifest === null) {
+        return false;
+    }
+    if (typeof rpc !== 'object' || rpc === null) {
+        return false;
+    }
+    if (typeof verbose !== 'boolean') {
+        if (verbose === 'true') {
+            verbose = true;
+        } else if (verbose === 'false') {
+            verbose = false;
+        }
+    }
+    return true;
+    }
+})
+}
+

File diff suppressed because it is too large
+ 3736 - 1810
package-lock.json


+ 6 - 4
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.3",
-  "description": "SNH-Oasis Project Networking GUI",
+  "version": "0.3.4",
+  "description": "SNH-Oasis Project Networking GUI Utopia",
   "repository": {
     "type": "git",
     "url": "git+ssh://git@code.03c8.net/krakenlabs/oasis.git"
@@ -22,7 +22,8 @@
     "start-client": "node src/index.js",
     "test": "tap --timeout 240 && common-good test",
     "preversion": "npm test",
-    "version": "mv docs/CHANGELOG.md ./ && changelog-version && mv CHANGELOG.md docs/ && git add docs/CHANGELOG.md"
+    "version": "mv docs/CHANGELOG.md ./ && mv CHANGELOG.md docs/ && git add docs/CHANGELOG.md",
+    "postinstall": "node ../../scripts/patch-node-modules.js"
   },
   "dependencies": {
     "@koa/router": "^13.1.0",
@@ -80,6 +81,7 @@
     "remark-html": "^16.0.1",
     "require-style": "^1.1.0",
     "secret-stack": "^6.3.1",
+    "sqlite3": "^5.1.7",
     "ssb-about": "^2.0.1",
     "ssb-backlinks": "^2.1.1",
     "ssb-blobs": "^2.0.1",
@@ -124,7 +126,7 @@
     "ssb-room": "^0.0.10",
     "ssb-search": "^1.3.0",
     "ssb-search2": "^2.1.3",
-    "ssb-server": "file:./packages/ssb-server",
+    "ssb-server": "file:packages/ssb-server",
     "ssb-tangle": "^1.0.1",
     "ssb-thread-schema": "^1.1.1",
     "ssb-threads": "^10.0.4",

+ 0 - 51
src/ssb/flotilla.js

@@ -1,51 +0,0 @@
-const SecretStack = require("secret-stack");
-const debug = require("debug")("oasis");
-const ssbConfig = require("ssb-config");
-
-const caps = require('ssb-caps')
-
-const plugins = [
-  // Authentication often hooked for authentication.
-  require("ssb-master"),
-  require("ssb-db"),
-  require("ssb-backlinks"),
-  require("ssb-conn"),
-  require("ssb-about"),
-  require("ssb-blobs"),
-  require("ssb-ebt"),
-  require("ssb-friends"),
-  require("ssb-replication-scheduler"),
-  require("ssb-invite"),
-  require("ssb-lan"),
-  require("ssb-logging"),
-  require("ssb-meme"),
-  require("ssb-no-auth"),
-  require("ssb-onion"),
-  require("ssb-ooo"),
-  require("ssb-plugins"),
-  require("ssb-private1"),
-  require("ssb-query"),
-  require("ssb-room/tunnel/client"),
-  require("ssb-search"),
-  require("ssb-tangle"),
-  require("ssb-unix-socket"),
-  require("ssb-ws"),
-];
-
-module.exports = (config) => {
-  const server = SecretStack({caps});
-  const walk = (input) => {
-
-    if (Array.isArray(input)) {
-      input.forEach(walk);
-    } else {
-      debug(input.name || "???");
-      server.use(input);
-    }
-  };
-
-  walk(plugins);
-
-  return server({ ...ssbConfig, ...config });
-};
-

+ 0 - 45
src/ssb/progress.js

@@ -1,45 +0,0 @@
-module.exports = function (progress) {
-  function bar (r) {
-    var s = '\r', M = 50
-    for(var i = 0; i < M; i++)
-      s += i < M*r ? '*' : '.'
-
-    return s
-  }
-
-  function round (n, p) {
-    return Math.round(n * p) / p
-  }
-
-  function percent (n) {
-    return (round(n, 1000)*100).toString().substring(0, 4)+'%'
-  }
-
-  function rate (prog) {
-    if(prog.target == prog.current) return 1
-    return (prog.current - prog.start) / (prog.target - prog.start)
-  }
-
-  var prog = -1
-  var int = setInterval(function () {
-    var p = progress()
-    var r = 1, c = 0
-    var tasks = []
-    for(var k in p) {
-      var _r = rate(p[k])
-      if(_r < 1)
-        tasks.push(k+':'+percent(_r))
-      r = Math.min(_r, r)
-      c++
-    }
-    if(r != prog) {
-      prog = r
-      var msg = tasks.join(', ')
-      process.stdout.write('\r'+bar(prog) + ' ('+msg+')\x1b[K\r')
-    }
-  }, 333)
-  int.unref && int.unref()
-}
-
-
-

+ 0 - 878
src/views/i18n.js

@@ -1,878 +0,0 @@
-const { a, em, strong } = require("hyperaxe");
-
-const i18n = {
-  en: {
-    // navbar items
-    extended: "Multiverse",
-    extendedDescription: [
-      "When you support someone you may download posts from the inhabitants they support, and those posts show up here, sorted by recency.",
-    ],
-    popular: "Highlights",
-    popularDescription: [
-      "Posts from inhabitants in your network, ",
-      strong("sorted by spreads"),
-      ". Select the period of time, to get a list.",
-    ],
-    day: "Day",
-    week: "Week",
-    month: "Month",
-    year: "Year",
-    latest: "Latest",
-    latestDescription: [
-      strong("Posts"),
-      " from yourself and inhabitants you support, sorted by recency.",
-    ],
-    topics: "Topics",
-    topicsDescription: [
-      strong("Topics"),
-      " from yourself and inhabitants you support, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
-    ],
-    summaries: "Summaries",
-    summariesDescription: [
-      strong("Topics and some comments"),
-      " from yourself and inhabitants you support, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
-    ],
-    threads: "Threads",
-    threadsDescription: [
-      strong("Posts that have comments"),
-      " from inhabitants you support and your multiverse, sorted by recency. Select the timestamp of any post to see the rest of the thread.",
-    ],
-    profile: "Avatar",
-    inhabitants: "Inhabitants", 
-    manualMode: "Manual Mode",
-    mentions: "Mentions",
-    mentionsDescription: [
-      strong("Posts that @mention you"),
-      ", sorted by recency.",
-    ],
-    private: "Inbox",
-    peers: "Peers",
-    privateDescription: [
-      "The latest comment from ",
-      strong("private threads that include you"),
-      ", sorted by recency. Private posts are encrypted for your public key, and have a maximum of 7 recipients. Recipients cannot be added after the thread has started. Select the timestamp to view the full thread.",
-    ],
-    search: "Search",
-    imageSearch: "Image Search",
-    settings: "Settings",
-    continueReading: "continue reading",
-    moreComments: "more comment",
-    readThread: "read the rest of the thread",    
-    // modules
-    modules: "Modules",
-    modulesViewTitle: "Modules",
-    modulesViewDescription: "Set your environment by enabling or disabling modules.",
-    inbox: "Inbox",
-    multiverse: "Multiverse",
-    popularLabel: "⌘ Highlights",
-    topicsLabel: "ϟ Topics",
-    latestLabel: "☄ Latest",
-    summariesLabel: "※ Summaries",
-    threadsLabel: "♺ Threads",
-    multiverseLabel: "∞ Multiverse",
-    inboxLabel: "☂ Inbox",
-    invitesLabel: "ꔹ Invites",
-    walletLabel: "❄ Wallet",
-    legacyLabel: "ꖸ Legacy",
-    cipherLabel: "ꗄ Crypter",
-    saveSettings: "Save configuration",
-    // post actions
-    comment: "Comment",
-    subtopic: "Subtopic",
-    json: "JSON",
-    // relationships
-    unfollow: "Unsupport",
-    follow: "Support",
-    block: "Block",
-    unblock: "Unblock",
-    newerPosts: "Newer posts",
-    olderPosts: "Older posts",
-    feedRangeEmpty: "The given range is empty for this feed. Try viewing the ",
-    seeFullFeed: "full feed",
-    feedEmpty: "The Oasis client has never seen posts from this account.",
-    beginningOfFeed: "This is the beginning of the feed",
-    noNewerPosts: "No newer posts have been received yet.",
-    relationshipNotFollowing: "You are not supported",
-    relationshipTheyFollow: "They support",
-    relationshipMutuals: "Mutual support",
-    relationshipFollowing: "You are supporting",
-    relationshipYou: "You",
-    relationshipBlocking: "You are blocking",
-    relationshipNone: "You are not supporting",
-    relationshipConflict: "Conflict",
-    relationshipBlockingPost: "Blocked post",
-    // spreads view
-    viewLikes: "View spreads",
-    spreadedDescription: "List of posts spread by the inhabitant.",
-    likedBy: " -> Spreads",
-    // composer
-    attachFiles: "Attach files",
-    mentionsMatching: "Matching mentions",
-    preview: "Preview",
-    publish: "Write",
-    contentWarningPlaceholder: "Add a subject to the post (optional)",
-    privateWarningPlaceholder: "Add inhabitants to send a private post (ex: @bob @alice) (optional)",
-    publishWarningPlaceholder: "...",
-    publishCustomDescription: [
-      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
-    ],
-    commentWarning: [
-      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
-    ],
-    commentPublic: "public",
-    commentPrivate: "private",
-    commentLabel: ({ publicOrPrivate, markdownUrl }) => [
-    ],
-    publishLabel: ({ markdownUrl, linkTarget }) => [
-      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
-    ],
-    replyLabel: ({ markdownUrl }) => [
-      "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.",
-    ],
-    publishCustomInfo: ({ href }) => [
-      "If you have experience, you can also ",
-      a({ href }, "write an advanced post"),
-      ".",
-    ],
-    publishBasicInfo: ({ href }) => [
-      "If you have not experience, you should ",
-      a({ href }, "write a post"),
-      ".",
-    ],
-    publishCustom: "Write advanced post",
-    subtopicLabel: ({ markdownUrl }) => [
-      "Create a ",
-      strong("public subtopic"),
-      " of this post with ",
-      a({ href: markdownUrl }, "Markdown"),
-      ". Posts cannot be edited or deleted. To respond to an entire thread, select ",
-      strong("comment"),
-      " instead. Preview shows attached media.",
-    ],
-    // settings
-    updateit: "Get updates",
-    info: "Info",
-    settingsIntro: ({ version }) => [
-      `Oasis: [${version}]`,
-    ],
-    timeAgo: "ago",
-    sendTime: "about ",
-    theme: "Theme",
-    legacy: "Legacy",
-    legacyTitle: "Legacy",
-    legacyDescription: "Manage your secret (private key) quickly and safely.",   
-    legacyExportButton: "Export",
-    legacyImportButton: "Import",
-    exportTitle: "Export data",
-    exportDescription: "Set password (min 32 characters long) to encrypt your key",
-    importTitle: "Import data",
-    importDescription: "Import your encrypted secret (private key) to enable your avatar",
-    importAttach: "Attach encrypted file (.enc)",    
-    passwordLengthInfo: "Password must be at least 32 characters long.",
-    passwordImport: "Write your password to decrypt data that will be saved at your system home (name: secret)",
-    randomPassword: "Random password",
-    exportPasswordPlaceholder: "Use lowercase, uppercase, numbers & symbols",
-    fileInfo: "Your encrypted secret key will be saved at your system home (name: oasis.enc)",
-    themeIntro:
-      "Choose a theme.",
-    setTheme: "Set theme",
-    language: "Language",
-    languageDescription:
-      "If you'd like to use another language, select it here.",
-    setLanguage: "Set language",
-    status: "Status",
-    peerConnections: "Peers",
-    peerConnectionsIntro: "Manage all your connections with other peers.",
-    online: "Online",
-    supported: "Supported",
-    recommended: "Recommended", 
-    blocked: "Blocked",
-    noConnections: "No peers connected.",
-    noSupportedConnections: "No peers supported.",
-    noBlockedConnections: "No peers blocked.",
-    noRecommendedConnections: "No peers recommended.",
-    connectionActionIntro:
-      "",
-    startNetworking: "Start networking",
-    stopNetworking: "Stop networking",
-    restartNetworking: "Restart networking",
-    sync: "Sync network",
-    indexes: "Indexes",
-    indexesDescription:
-      "Rebuilding your indexes is safe, and may fix some types of bugs.",
-    invites: "Invites",
-    invitesDescription:
-      "Use the PUB's invite codes here.",
-    acceptInvite: "Accept invite",
-    acceptedInvites: "Federated Networks",
-    noInvites: "No invites accepted.",
-    // search page
-    searchLabel: "Seek inhabitants and keywords, among the posts you have downloaded.",
-    // image search page
-    imageSearchLabel: "Enter words to search for images labelled with them.",
-    // posts and comments
-    commentDescription: ({ parentUrl }) => [
-      " commented on ",
-      a({ href: parentUrl }, " thread"),
-    ],
-    commentTitle: ({ authorName }) => [`Comment on @${authorName}'s post`],
-    subtopicDescription: ({ parentUrl }) => [
-      " created a subtopic from ",
-      a({ href: parentUrl }, " a post"),
-    ],
-    subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`],
-    mysteryDescription: "posted a mysterious post",
-    // misc
-    oasisDescription: "SNH Project Network",
-    submit: "Submit",
-    subjectLabel: "Subject",
-    editProfile: "Edit Avatar",
-    editProfileDescription:
-      "",
-    profileName: "Avatar name (plain text)",
-    profileImage: "Avatar image",
-    profileDescription: "Avatar description (Markdown)",
-    hashtagDescription:
-      "Posts from inhabitants in your network that reference this #hashtag, sorted by recency.",
-    rebuildName: "Rebuild database",
-    wallet: "Wallet",
-    walletAddress: "Address",
-    walletAmount: "Amount",
-    walletAddressLine: ({ address }) => `Address: ${address}`,
-    walletAmountLine: ({ amount }) => `Amount: ${amount} ECO`,
-    walletBack: "Back",
-    walletBalanceTitle: "Balance",
-    walletWalletSendTitle: "Send",
-    walletReceiveTitle: "Receive",
-    walletHistoryTitle: "History",
-    walletBalanceLine: ({ balance }) => `${balance} ECO`,
-    walletCnfrs: "Cnfrs",
-    walletConfirm: "Confirm",
-    walletDescription: "Manage your digital assets, including sending and receiving ECOin, viewing your balance, and accessing your transaction history.",
-    walletDate: "Date",
-    walletFee: "Fee (The higher the fee, the faster your transaction will be processed)",
-    walletFeeLine: ({ fee }) => `Fee: ECO ${fee}`,
-    walletHistory: "History",
-    walletReceive: "Receive",
-    walletReset: "Reset",
-    walletSend: "Send",
-    walletStatus: "Status",
-    walletDisconnected: "ECOin wallet disconnected. Check your wallet settings or connection status.",
-    walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`,
-    walletSettingsTitle: "Wallet",
-    walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
-    walletStatusMessages: {
-      invalid_amount: "Invalid amount",
-      invalid_dest: "Invalid destination address",
-      invalid_fee: "Invalid fee",
-      validation_errors: "Validation errors",
-      send_tx_success: "Transaction successful",
-    },
-    walletTitle: "Wallet",
-    walletTotalCostLine: ({ totalCost }) => `Total cost: ECO ${totalCost}`,
-    walletTransactionId: "Transaction ID",
-    walletTxId: "Tx ID",
-    walletType: "Type",
-    walletUser: "Username",
-    walletPass: "Password",
-    walletConfiguration: "Set wallet",
-    //cipher
-    cipher: "Crypter",
-    cipherTitle: "Cipher",
-    cipherDescription: "Encrypt and decrypt your text symmetrically (using a shared password).",
-    randomPassword: "Random Password",
-    cipherEncryptTitle: "Encrypt Text",
-    cipherEncryptDescription: "Set password (min 32 characters long) to encrypt your text",
-    cipherTextLabel: "Text to Encrypt",
-    cipherTextPlaceholder: "Enter text to encrypt...",
-    cipherPasswordLabel: "Password",
-    cipherPasswordPlaceholder: "Enter a password...",
-    cipherEncryptButton: "Encrypt",
-    cipherDecryptTitle: "Decrypt Text",
-    cipherDecryptDescription: "Enter the encrypted text and password to decrypt.",
-    cipherEncryptedTextLabel: "Encrypted Text",
-    cipherEncryptedTextPlaceholder: "Enter the encrypted text...",
-    cipherIvLabel: "IV",
-    cipherIvPlaceholder: "Enter the initialization vector...",
-    cipherDecryptButton: "Decrypt",
-    password: "Password",
-    text: "Text",
-    encryptedText: "Encrypted Text",
-    iv: "Initialization Vector (IV)",
-    encryptTitle: "Encrypt your text",
-    encryptDescription: "Enter the text you want to encrypt and provide a password.",
-    encryptButton: "Encrypt",
-    decryptTitle: "Decrypt your text",
-    decryptDescription: "Enter the encrypted text and provide the same password used for encryption.",
-    decryptButton: "Decrypt",
-    passwordLengthError: "Password must be at least 32 characters long.",
-    missingFieldsError: "Text, password or IV not provided.",
-    encryptionError: "Error encrypting text.",
-    decryptionError: "Error decrypting text."
-    },
-  /* spell-checker: disable */
-    es: {
-    // navbar items
-    extended: "Multiverso",
-    extendedDescription: [
-    "Cuando apoyas a alguien, puedes descargar sus publicaciones, y esas publicaciones aparecen aquí, ordenadas por las más recientes.",
-    ],
-    popular: "Destacados",
-    popularDescription: [
-    "Publicaciones de habitantes en tu red, ",
-    strong("ordenadas por expansiones"),
-    ". Selecciona el período de tiempo para obtener una lista.",
-    ],
-    day: "Día",
-    week: "Semana",
-    month: "Mes",
-    year: "Año",
-    latest: "Últimos",
-    latestDescription: [
-    strong("Publicaciones"),
-    " tuyas y de lass habitantes que apoyas, ordenadas por la más reciente.",
-    ],
-    topics: "Temas",
-    topicsDescription: [
-    strong("Temas"),
-    " tuyos y de las habitantes que apoyas, ordenados por los más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
-    ],
-    summaries: "Resúmenes",
-    summariesDescription: [
-    strong("Temas y algunos comentarios"),
-    " tuyos y de las habitantes que apoyas, ordenados por los más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
-    ],
-    threads: "Hilos",
-    threadsDescription: [
-    strong("Publicaciones que tienen comentarios"),
-    " de las habitantes que apoyas y tu multiverso, ordenadas por las más recientes. Selecciona la marca de tiempo de cualquier publicación para ver el resto del hilo.",
-    ],
-    profile: "Avatar",
-    inhabitants: "Habitantes",
-    manualMode: "Modo Manual",
-    mentions: "Menciones",
-    mentionsDescription: [
-    strong("Publicaciones que te @mencionan"),
-    ", ordenadas por las más recientes.",
-    ],
-    private: "Buzón",
-    peers: "Nodos",
-    privateDescription: [
-    "El último comentario de ",
-    strong("hilos privados que te incluyen"),
-    ", ordenados por los más recientes. Las publicaciones privadas están cifradas para tu clave pública, y tienen un máximo de 7 destinatarios. Los destinatarios no pueden ser añadidos después de que el hilo haya comenzado. Selecciona la marca de tiempo para ver el hilo completo.",
-    ],
-    search: "Buscar",
-    imageSearch: "Búsqueda de imágenes",
-    settings: "Configuración",
-    continueReading: "seguir leyendo",
-    moreComments: "comentarios má",
-    readThread: "leer el resto del hilo",
-    // modules
-    modules: "Módulos",
-    modulesViewTitle: "Módulos",
-    modulesViewDescription: "Configura tu entorno habilitando o deshabilitando módulos.",
-    inbox: "Buzón",
-    multiverse: "Multiverso",
-    popularLabel: "⌘ Destacados",
-    topicsLabel: "ϟ Temas",
-    latestLabel: "☄ Últimos",
-    summariesLabel: "※ Resúmenes",
-    threadsLabel: "♺ Hilos",
-    multiverseLabel: "∞ Multiverso",
-    inboxLabel: "☂ Buzón",
-    invitesLabel: "ꔹ Invitaciones",
-    walletLabel: "❄ Billetera",
-    legacyLabel: "ꖸ Herencia",
-    cipherLabel: "ꗄ Cripta",
-    saveSettings: "Guardar configuración",
-    // post actions
-    comment: "Comentar",
-    subtopic: "Subtema",
-    json: "JSON",
-    // relationships
-    unfollow: "Dejar de apoyar",
-    follow: "Apoyar",
-    block: "Bloquear",
-    unblock: "Desbloquear",
-    newerPosts: "Publicaciones más recientes",
-    olderPosts: "Publicaciones más antiguas",
-    feedRangeEmpty: "El rango dado está vacío para este feed. Intenta ver el ",
-    seeFullFeed: "feed completo",
-    feedEmpty: "El cliente local de Oasis no ha accedido a publicaciones aún.",
-    beginningOfFeed: "Este es el comienzo del feed",
-    noNewerPosts: "No se han recibido publicaciones más recientes.",
-    relationshipNotFollowing: "No te da apoyo",
-    relationshipTheyFollow: "Ellos apoyan",
-    relationshipMutuals: "Apoyo mutuo",
-    relationshipFollowing: "Das apoyo",
-    relationshipYou: "Tú",
-    relationshipBlocking: "Estás bloqueando",
-    relationshipNone: "No estás apoyando",
-    relationshipConflict: "En conflicto",
-    relationshipBlockingPost: "Publicación bloqueada",
-    // spreads view
-    viewLikes: "Ver Apoyos",
-    spreadedDescription: "Lista de publicaciones apoyadas por la habitante.",
-    likedBy: " -> Apoyos",
-    // composer
-    attachFiles: "Adjuntar archivos",
-    mentionsMatching: "Menciones coincidentes",
-    preview: "Vista previa",
-    publish: "Escribir",
-    contentWarningPlaceholder: "Agrega un tema a la publicación (opcional)",
-    privateWarningPlaceholder: "Agrega habitantes para enviar una publicación privada (ej: @bob @alice) (opcional)",
-    publishWarningPlaceholder: "...",
-    publishCustomDescription: [
-    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
-    ],
-    commentWarning: [
-    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
-    ],
-    commentPublic: "público",
-    commentPrivate: "privado",
-    commentLabel: ({ publicOrPrivate, markdownUrl }) => [
-    ],
-    publishLabel: ({ markdownUrl, linkTarget }) => [
-    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
-    ],
-    replyLabel: ({ markdownUrl }) => [
-    "RECUERDA: Debido a la tecnología blockchain, una vez que se publica no se puede editar ni eliminar.",
-    ],
-    publishCustomInfo: ({ href }) => [
-    "Si tienes experiencia, también puedes ",
-    a({ href }, "escribir una publicación avanzada"),
-    ".",
-    ],
-    publishBasicInfo: ({ href }) => [
-      "Si no tienes experiencia, deberías ",
-      a({ href }, "escribir una publicación"),
-      ".",
-    ],
-    publishCustom: "Escribir publicación avanzada",
-    subtopicLabel: ({ markdownUrl }) => [
-      "Crear un ",
-      strong("subtema público"),
-      " de esta publicación con ",
-      a({ href: markdownUrl }, "Markdown"),
-      ". Las publicaciones no pueden ser editadas ni eliminadas. Para responder a un hilo completo, selecciona ",
-      strong("comentar"),
-    " en su lugar. La vista previa muestra los medios adjuntos.",
-    ],
-    // settings
-    updateit: "Obtener actualizaciones",
-    info: "Información",
-    settingsIntro: ({ version }) => [
-      `Oasis: [${version}]`,
-    ],
-    timeAgo: "",
-    sendTime: "aproximadamente hace ",
-    theme: "Tema",
-    legacy: "Herencia",
-    legacyTitle: "Herencia",
-    legacyDescription: "Gestiona tu secreto (clave privada) de forma rápida y segura.",
-    legacyExportButton: "Exportar",
-    legacyImportButton: "Importar",
-    exportTitle: "Exportar datos",
-    exportDescription: "Establece una contraseña (mínimo 32 caracteres) para cifrar tu clave",
-    importTitle: "Importar datos",
-    importDescription: "Importa tu secreto cifrado (clave privada) para habilitar tu avatar",
-    importAttach: "Adjuntar archivo cifrado (.enc)",
-    passwordLengthInfo: "La contraseña debe tener al menos 32 caracteres.",
-    passwordImport: "Escribe tu contraseña para descifrar los datos que se guardarán en el directorio de tu sistema (nombre: secreto)",
-    exportPasswordPlaceholder: "Usa minúsculas, mayúsculas, números y símbolos",
-    fileInfo: "Tu clave secreta cifrada se guardará en el directorio de tu sistema (nombre: oasis.enc)",
-    themeIntro: "Elige un tema.",
-    setTheme: "Establecer tema",
-    language: "Idioma",
-    languageDescription: "Si deseas usar otro idioma, selecciona aquí.",
-    setLanguage: "Establecer idioma",
-    status: "Estado",
-    peerConnections: "Nodos",
-    peerConnectionsIntro: "Maneja todas tus conexiones que otros nodos.",
-    online: "En línea",
-    supported: "Apoyado",
-    recommended: "Recomendado",
-    blocked: "Bloqueado",
-    noConnections: "No hay habitantes conectados.",
-    noSupportedConnections: "No hay habitantes apoyados.",
-    noBlockedConnections: "No hay habitantes bloqueados.",
-    noRecommendedConnections: "No hay habitantes recomendados.",
-    connectionActionIntro: "",
-    startNetworking: "Comenzar a conectar",
-    stopNetworking: "Dejar de conectar",
-    restartNetworking: "Reiniciar la conexión",
-    sync: "Sincronizar red",
-    indexes: "Índices",
-    indexesDescription: "Reconstruir tus índices es seguro y puede solucionar algunos tipos de errores.",
-    invites: "Invitaciones",
-    invitesDescription: "Usa los códigos de invitación del PUB aquí.",
-    acceptInvite: "Aceptar invitación",
-    acceptedInvites: "Redes federadas",
-    noInvites: "No se han aceptado invitaciones",
-    // search page
-    searchLabel: "Buscar habitantes y palabras clave entre las publicaciones que has descargado.",
-    // image search page
-    imageSearchLabel: "Introduce palabras para buscar imágenes etiquetadas con ellas.",
-    // posts and comments
-    commentDescription: ({ parentUrl }) => [
-    " comentó en ",
-    a({ href: parentUrl }, " el hilo"),
-    ],
-    commentTitle: ({ authorName }) => [`Comentario en la publicación de @${authorName}`],
-    subtopicDescription: ({ parentUrl }) => [
-    " creó un subtema de ",
-    a({ href: parentUrl }, " una publicación"),
-    ],
-    subtopicTitle: ({ authorName }) => [`Subtema en la publicación de @${authorName}`],
-    mysteryDescription: "envió una publicación misteriosa",
-    // misc
-    oasisDescription: "Red del Proyecto SNH",
-    submit: "Enviar",
-    subjectLabel: "Asunto",
-    editProfile: "Editar Avatar",
-    editProfileDescription: "",
-    profileName: "Nombre del avatar (texto plano)",
-    profileImage: "Imagen del avatar",
-    profileDescription: "Descripción del avatar (Markdown)",
-    hashtagDescription: "Publicaciones de habitantes en tu red que mencionan este #hashtag, ordenadas por las más recientes.",
-    rebuildName: "Reconstruir base de datos",
-    wallet: "Billetera",
-    walletAddress: "Dirección",
-    walletAmount: "Cantidad",
-    walletAddressLine: ({ address }) => `Dirección: ${address}`,
-    walletAmountLine: ({ amount }) => `Cantidad: ${amount} ECO`,
-    walletBack: "Volver",
-    walletBalanceTitle: "Balance",
-    walletReceiveTitle: "Recibir",
-    walletHistoryTitle: "Historial",
-    walletWalletSendTitle: "Enviar",
-    walletBalanceLine: ({ balance }) => `${balance} ECO`,
-    walletCnfrs: "Cnfrs",
-    walletConfirm: "Confirmar",
-    walletDescription: "Gestiona tus activos digitales, incluyendo el envío y recepción de ECOin, ver tu saldo e historial de transacciones.",
-    walletDate: "Fecha",
-    walletFee: "Comisión (Cuanto mayor es la comisión, más rápido se procesará tu transacción)",
-    walletFeeLine: ({ fee }) => `Comisión: ECO ${fee}`,
-    walletHistory: "Historial",
-    walletReceive: "Recibir",
-    walletReset: "Restablecer",
-    walletSend: "Enviar",
-    walletStatus: "Estado",
-    walletDisconnected: "Billetera ECOin desconectada. Verifica la configuración de tu billetera o el estado de la conexión.",
-    walletSentToLine: ({ destination, amount }) => `Enviado ECO ${amount} a ${destination}`,
-    walletSettingsTitle: "Billetera",
-    walletSettingsDescription: "Integra Oasis con tu billetera ECOin.",
-    walletStatusMessages: {
-    invalid_amount: "Cantidad inválida",
-    invalid_dest: "Dirección de destino inválida",
-    invalid_fee: "Comisión inválida",
-    validation_errors: "Errores de validación",
-    send_tx_success: "Transacción exitosa",
-    },
-    walletTitle: "Billetera",
-    walletTotalCostLine: ({ totalCost }) => `Costo total: ECO ${totalCost}`,
-    walletTransactionId: "ID de transacción",
-    walletTxId: "ID Tx",
-    walletType: "Tipo",
-    walletUser: "Nombre de usuario",
-    walletPass: "Contraseña",
-    walletConfiguration: "Configurar billetera",
-    //cipher
-    cipher: "Cripta",
-    randomPassword: "Contraseña aleatoria",
-    password: "Contraseña",
-    text: "Texto",
-    encryptedText: "Texto cifrado",
-    iv: "Vector de Inicialización (IV)",
-    encryptTitle: "Cifra tu texto",
-    encryptDescription: "Introduce el texto que deseas cifrar y proporciona una contraseña.",
-    encryptButton: "Cifrar",
-    decryptTitle: "Descifra tu texto",
-    decryptDescription: "Introduce el texto cifrado y proporciona la misma contraseña utilizada para cifrar.",
-    decryptButton: "Descifrar",
-    passwordLengthError: "La contraseña debe tener al menos 32 caracteres.",
-    missingFieldsError: "Texto, contraseña o IV no proporcionados.",
-    encryptionError: "Error cifrando el texto.",
-    decryptionError: "Error descifrando el texto.",
-    //cipher
-    cipherTitle: "Cifrado",
-    cipherDescription: "Cifra y descifra contenido simétricamente (usando una contraseña compartida).",
-    randomPassword: "Contraseña Aleatoria",
-    cipherEncryptTitle: "Encriptar Texto",
-    cipherEncryptDescription: "Establece una contraseña (mínimo 32 caracteres) para cifrar tu texto",
-    cipherTextLabel: "Texto a Encriptar",
-    cipherTextPlaceholder: "Introduce el texto para encriptar...",
-    cipherPasswordLabel: "Contraseña",
-    cipherPasswordPlaceholder: "Introduce una contraseña...",
-    cipherEncryptButton: "Encriptar",
-    cipherDecryptTitle: "Desencriptar Texto",
-    cipherDecryptDescription: "Introduce el texto encriptado, la contraseña y el IV para desencriptar.",
-    cipherEncryptedTextLabel: "Texto Encriptado",
-    cipherEncryptedTextPlaceholder: "Introduce el texto encriptado...",
-    cipherIvLabel: "IV",
-    cipherIvPlaceholder: "Introduce el vector de inicialización...",
-    cipherDecryptButton: "Desencriptar"
-  },
-  /* spell-checker: disable */
-fr: {
-    // navbar items
-    extended: "Multivers",
-    extendedDescription: [
-    "Lorsque vous soutenez quelqu'un, vous pouvez télécharger ses publications, et ces publications apparaissent ici, triées par les plus récentes.",
-    ],
-    popular: "En vedette",
-    popularDescription: [
-    "Publications des habitants de votre réseau, ",
-    strong("classées par expansions"),
-    ". Sélectionnez la période pour obtenir une liste.",
-    ],
-    day: "Jour",
-    week: "Semaine",
-    month: "Mois",
-    year: "Année",
-    latest: "Derniers",
-    latestDescription: [
-    strong("Publications"),
-    " de vous et des habitants que vous soutenez, triées par les plus récentes.",
-    ],
-    topics: "Sujets",
-    topicsDescription: [
-    strong("Sujets"),
-    " de vous et des habitants que vous soutenez, triés par les plus récents. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
-    ],
-    summaries: "Résumés",
-    summariesDescription: [
-    strong("Sujets et quelques commentaires"),
-    " de vous et des habitants que vous soutenez, triés par les plus récents. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
-    ],
-    threads: "Fils",
-    threadsDescription: [
-    strong("Publications avec commentaires"),
-    " des habitants que vous soutenez et de votre multivers, triées par les plus récentes. Sélectionnez l'horodatage d'une publication pour voir le reste du fil.",
-    ],
-    profile: "Avatar",
-    inhabitants: "Habitants",
-    manualMode: "Mode Manuel",
-    mentions: "Mentions",
-    mentionsDescription: [
-    strong("Publications qui vous @mentionnent"),
-    ", triées par les plus récentes.",
-    ],
-    private: "Boîte de réception",
-    peers: "Nœuds",
-    privateDescription: [
-    "Le dernier commentaire de ",
-    strong("fils privés qui vous incluent"),
-    ", triés par les plus récents. Les publications privées sont chiffrées pour votre clé publique et ont un maximum de 7 destinataires. Les destinataires ne peuvent pas être ajoutés après le début du fil. Sélectionnez l'horodatage pour voir le fil complet.",
-    ],
-    search: "Rechercher",
-    imageSearch: "Recherche d'images",
-    settings: "Paramètres",
-    continueReading: "continuer la lecture",
-    moreComments: "plus de commentaires",
-    readThread: "lire le reste du fil",
-    // modules
-    modules: "Modules",
-    modulesViewTitle: "Modules",
-    modulesViewDescription: "Configurez votre environnement en activant ou désactivant des modules.",
-    inbox: "Boîte de réception",
-    multiverse: "Multivers",
-    popularLabel: "⌘ En vedette",
-    topicsLabel: "ϟ Sujets",
-    latestLabel: "☄ Derniers",
-    summariesLabel: "※ Résumés",
-    threadsLabel: "♺ Fils",
-    multiverseLabel: "∞ Multivers",
-    inboxLabel: "☂ Boîte de réception",
-    invitesLabel: "ꔹ Invitations",
-    walletLabel: "❄ Portefeuille",
-    legacyLabel: "ꖸ Héritage",
-    cipherLabel: "ꗄ Crypte",
-    saveSettings: "Enregistrer les paramètres",
-    // post actions
-    comment: "Commenter",
-    subtopic: "Sous-sujet",
-    json: "JSON",
-    // relationships
-    unfollow: "Ne plus soutenir",
-    follow: "Soutenir",
-    block: "Bloquer",
-    unblock: "Débloquer",
-    newerPosts: "Publications plus récentes",
-    olderPosts: "Publications plus anciennes",
-    feedRangeEmpty: "La plage donnée est vide pour ce flux. Essayez de voir le ",
-    seeFullFeed: "flux complet",
-    feedEmpty: "Le client local d'Oasis n'a pas encore accédé aux publications.",
-    beginningOfFeed: "Ceci est le début du flux",
-    noNewerPosts: "Aucune publication plus récente reçue.",
-    relationshipNotFollowing: "Ne vous soutient pas",
-    relationshipTheyFollow: "Ils soutiennent",
-    relationshipMutuals: "Soutien mutuel",
-    relationshipFollowing: "Vous soutenez",
-    relationshipYou: "Vous",
-    relationshipBlocking: "Vous bloquez",
-    relationshipNone: "Vous ne soutenez pas",
-    relationshipConflict: "En conflit",
-    relationshipBlockingPost: "Publication bloquée",
-    // spreads view
-    viewLikes: "Voir les soutiens",
-    spreadedDescription: "Liste des publications soutenues par l'habitant.",
-    likedBy: " -> Soutiens",
-    // composer
-    attachFiles: "Joindre des fichiers",
-    mentionsMatching: "Mentions correspondantes",
-    preview: "Aperçu",
-    publish: "Publier",
-    contentWarningPlaceholder: "Ajoutez un sujet à la publication (optionnel)",
-    privateWarningPlaceholder: "Ajoutez des habitants pour envoyer une publication privée (ex: @bob @alice) (optionnel)",
-    publishWarningPlaceholder: "...",
-    publishCustomDescription: [
-    "RAPPEL : En raison de la technologie blockchain, une fois publié, il ne peut être ni modifié ni supprimé.",
-    ],
-    commentWarning: [
-    "RAPPEL : En raison de la technologie blockchain, une fois publié, il ne peut être ni modifié ni supprimé.",
-    ],
-    commentPublic: "public",
-    commentPrivate: "privé",
-    // settings
-    updateit: "Obtenir les mises à jour",
-    info: "Informations",
-    settingsIntro: ({ version }) => [
-      `Oasis: [${version}]`,
-    ],
-    timeAgo: "",
-    sendTime: "environ ",
-    theme: "Thème",
-    legacy: "Héritage",
-    legacyTitle: "Héritage",
-    legacyDescription: "Gérez votre secret (clé privée) rapidement et en toute sécurité.",
-    legacyExportButton: "Exporter",
-    legacyImportButton: "Importer",
-    exportTitle: "Exporter les données",
-    exportDescription: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre clé",
-    importTitle: "Importer des données",
-    importDescription: "Importez votre secret chiffré (clé privée) pour activer votre avatar",
-    importAttach: "Joindre un fichier chiffré (.enc)",
-    passwordLengthInfo: "Le mot de passe doit contenir au moins 32 caractères.",
-    passwordImport: "Saisissez votre mot de passe pour déchiffrer les données qui seront enregistrées dans le répertoire de votre système (nom : secret)",
-    exportPasswordPlaceholder: "Utilisez des minuscules, majuscules, chiffres et symboles",
-    fileInfo: "Votre clé secrète chiffrée sera enregistrée dans le répertoire de votre système (nom : oasis.enc)",
-    themeIntro: "Choisissez un thème.",
-    setTheme: "Définir le thème",
-    language: "Langue",
-    languageDescription: "Si vous souhaitez utiliser une autre langue, sélectionnez-la ici.",
-    setLanguage: "Définir la langue",
-    status: "Statut",
-    peerConnections: "Nœuds",
-    peerConnectionsIntro: "Gérez toutes vos connexions avec d'autres nœuds.",
-    online: "En ligne",
-    supported: "Soutenu",
-    recommended: "Recommandé",
-    blocked: "Bloqué",
-    noConnections: "Aucun habitant connecté.",
-    noSupportedConnections: "Aucun habitant soutenu.",
-    noBlockedConnections: "Aucun habitant bloqué.",
-    noRecommendedConnections: "Aucun habitant recommandé.",
-    startNetworking: "Commencer à se connecter",
-    stopNetworking: "Arrêter de se connecter",
-    restartNetworking: "Redémarrer la connexion",
-    sync: "Synchroniser le réseau",
-    indexes: "Index",
-    indexesDescription: "Reconstruire vos index est sûr et peut résoudre certains types d'erreurs.",
-    invites: "Invitations",
-    invitesDescription: "Utilisez les codes d'invitation du PUB ici.",
-    acceptInvite: "Accepter l'invitation",
-    acceptedInvites: "Réseaux fédérés",
-    noInvites: "Aucune invitation acceptée",
-    // misc
-    oasisDescription: "Réseau du Projet SNH",
-    submit: "Envoyer",
-    subjectLabel: "Sujet",
-    editProfile: "Modifier l'avatar",
-    editProfileDescription: "",
-    profileName: "Nom de l'avatar (texte brut)",
-    profileImage: "Image de l'avatar",
-    profileDescription: "Description de l'avatar (Markdown)",
-    hashtagDescription: "Publications des habitants de votre réseau mentionnant ce #hashtag, triées par les plus récentes.",
-    rebuildName: "Reconstruire la base de données",
-    wallet: "Portefeuille",
-    walletAddress: "Adresse",
-    walletAmount: "Montant",
-    walletAddressLine: ({ address }) => `Adresse : ${address}`,
-    walletAmountLine: ({ amount }) => `Montant : ${amount} ECO`,
-    walletBack: "Retour",
-    walletBalanceTitle: "Solde",
-    walletReceiveTitle: "Recevoir",
-    walletHistoryTitle: "Historique",
-    walletWalletSendTitle: "Envoyer",
-    walletBalanceLine: ({ balance }) => `${balance} ECO`,
-    walletCnfrs: "Cnfrs",
-    walletConfirm: "Confirmer",
-    walletDescription: "Gérez vos actifs numériques, y compris l'envoi et la réception d'ECOin, consultez votre solde et votre historique de transactions.",
-    walletDate: "Date",
-    walletFee: "Frais (Plus les frais sont élevés, plus votre transaction sera traitée rapidement)",
-    walletFeeLine: ({ fee }) => `Frais : ECO ${fee}`,
-    walletHistory: "Historique",
-    walletReceive: "Recevoir",
-    walletReset: "Réinitialiser",
-    walletSend: "Envoyer",
-    walletStatus: "Statut",
-    walletDisconnected: "Portefeuille ECOin déconnecté. Vérifiez la configuration de votre portefeuille ou l'état de la connexion.",
-    walletSentToLine: ({ destination, amount }) => `Envoyé ECO ${amount} à ${destination}`,
-    walletSettingsTitle: "Portefeuille",
-    walletSettingsDescription: "Intégrez Oasis à votre portefeuille ECOin.",
-    walletStatusMessages: {
-        invalid_amount: "Montant invalide",
-        invalid_dest: "Adresse de destination invalide",
-        invalid_fee: "Frais invalides",
-        validation_errors: "Erreurs de validation",
-        send_tx_success: "Transaction réussie",
-    },
-    walletTitle: "Portefeuille",
-    walletTotalCostLine: ({ totalCost }) => `Coût total : ECO ${totalCost}`,
-    walletTransactionId: "ID de transaction",
-    walletTxId: "ID Tx",
-    walletType: "Type",
-    walletUser: "Nom d'utilisateur",
-    walletPass: "Mot de passe",
-    walletConfiguration: "Configurer le portefeuille",
-    //cipher
-    cipher: "Chiffrement",
-    randomPassword: "Mot de passe aléatoire",
-    password: "Mot de passe",
-    text: "Texte",
-    encryptedText: "Texte chiffré",
-    iv: "Vecteur d'initialisation (IV)",
-    encryptTitle: "Chiffrez votre texte",
-    encryptDescription: "Entrez le texte que vous souhaitez chiffrer et fournissez un mot de passe.",
-    encryptButton: "Chiffrer",
-    decryptTitle: "Déchiffrez votre texte",
-    decryptDescription: "Entrez le texte chiffré et fournissez le même mot de passe utilisé pour le chiffrer.",
-    decryptButton: "Déchiffrer",
-    passwordLengthError: "Le mot de passe doit contenir au moins 32 caractères.",
-    missingFieldsError: "Texte, mot de passe ou IV non fournis.",
-    encryptionError: "Erreur lors du chiffrement du texte.",
-    decryptionError: "Erreur lors du déchiffrement du texte.",
-    //cipher
-    cipherTitle: "Chiffrement",
-    cipherDescription: "Chiffrez et déchiffrez du contenu de manière symétrique (en utilisant un mot de passe partagé).",
-    randomPassword: "Mot de passe aléatoire",
-    cipherEncryptTitle: "Chiffrer un texte",
-    cipherEncryptDescription: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre texte",
-    cipherTextLabel: "Texte à chiffrer",
-    cipherTextPlaceholder: "Entrez le texte à chiffrer...",
-    cipherPasswordLabel: "Mot de passe",
-    cipherPasswordPlaceholder: "Entrez un mot de passe...",
-    cipherEncryptButton: "Chiffrer",
-    cipherDecryptTitle: "Déchiffrer un texte",
-    cipherDecryptDescription: "Entrez le texte chiffré, le mot de passe et l'IV pour le déchiffrer.",
-    cipherEncryptedTextLabel: "Texte chiffré",
-    cipherEncryptedTextPlaceholder: "Entrez le texte chiffré...",
-    cipherIvLabel: "IV",
-    cipherIvPlaceholder: "Entrez le vecteur d'initialisation...",
-    cipherDecryptButton: "Déchiffrer"
-    }
-};
-
-module.exports = i18n;

+ 107 - 85
src/views/index.js

@@ -1,15 +1,15 @@
 "use strict";
 
 const path = require("path");
-const envPaths = require("env-paths");
+const envPaths = require("../server/node_modules/env-paths");
 const fs = require("fs");
 const homedir = require('os').homedir();
 const gossipPath = path.join(homedir, ".ssb/gossip.json");
-const debug = require("debug")("oasis");
-const highlightJs = require("highlight.js");
-const prettyMs = require("pretty-ms");
+const debug = require("../server/node_modules/debug")("oasis");
+const highlightJs = require("../server/node_modules/highlight.js");
+const prettyMs = require("../server/node_modules/pretty-ms");
 
-const updater = require("../updater.js");
+const updater = require("../backend/updater.js");
 async function checkForUpdate() {
   try {
     await updater.getRemoteVersion();
@@ -73,12 +73,12 @@ const {
   title,
   tr,
   ul,
-} = require("hyperaxe");
+} = require("../server/node_modules/hyperaxe");
 
-const lodash = require("lodash");
+const lodash = require("../server/node_modules/lodash");
 const markdown = require("./markdown");
 
-const i18nBase = require("./i18n");
+const i18nBase = require("../client/assets/translations/i18n");
 
 let selectedLanguage = "en";
 let i18n = i18nBase[selectedLanguage];
@@ -94,17 +94,17 @@ const snhUrl = "https://solarnethub.com/";
 const doctypeString = "<!DOCTYPE html>";
 
 const THREAD_PREVIEW_LENGTH = 3;
-
 const toAttributes = (obj) =>
   Object.entries(obj)
     .map(([key, val]) => `${key}=${val}`)
     .join(", ");
-
-// non-breaking space
+    
 const nbsp = "\xa0";
 
-const { saveConfig, getConfig } = require('../config');
+const { saveConfig, getConfig } = require('../configs/config-manager.js');
 const configMods = getConfig().modules;
+
+// menu INIT
 const navLink = ({ href, emoji, text, current }) =>
   li(
     a(
@@ -135,24 +135,28 @@ const renderPopularLink = () => {
     ? navLink({ href: "/public/popular/day", emoji: "⌘", text: i18n.popular, class: "popular-link enabled" }) 
     : ''; 
 };
+
 const renderTopicsLink = () => {
   const topicsMod = getConfig().modules.topicsMod === 'on';
   return topicsMod 
     ? navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics, class: "topics-link enabled" }) 
     : ''; 
 };
+
 const renderSummariesLink = () => {
   const summariesMod = getConfig().modules.summariesMod === 'on';
   return summariesMod 
     ? navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries, class: "summaries-link enabled" }) 
     : ''; 
 };
+
 const renderLatestLink = () => {
   const latestMod = getConfig().modules.latestMod === 'on';
   return latestMod 
     ? navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest, class: "latest-link enabled" }) 
     : ''; 
 };
+
 const renderThreadsLink = () => {
   const threadsMod = getConfig().modules.threadsMod === 'on';
   return threadsMod 
@@ -166,12 +170,14 @@ const renderInboxLink = () => {
     ? navLink({ href: "/inbox", emoji: "☂", text: i18n.inbox, class: "inbox-link enabled" }) 
     : ''; 
 };
+
 const renderInvitesLink = () => {
   const invitesMod = getConfig().modules.invitesMod === 'on';
   return invitesMod 
     ? navLink({ href: "/invites", emoji: "ꔹ", text: i18n.invites, class: "invites-link enabled" }) 
     : ''; 
 };
+
 const renderMultiverseLink = () => {
   const multiverseMod = getConfig().modules.multiverseMod === 'on';
   return multiverseMod 
@@ -181,6 +187,7 @@ const renderMultiverseLink = () => {
       ]
     : '';
 };
+
 const renderWalletLink = () => {
   const walletMod = getConfig().modules.walletMod === 'on';
   if (walletMod) {
@@ -190,15 +197,17 @@ const renderWalletLink = () => {
   }
   return ''; 
 };
+
 const renderLegacyLink = () => {
   const legacyMod = getConfig().modules.legacyMod === 'on';
   if (legacyMod) {
     return [
-      navLink({ href: "/legacy", emoji: "", text: i18n.legacy, class: "legacy-link enabled" }),
+      navLink({ href: "/legacy", emoji: "", text: i18n.legacy, class: "legacy-link enabled" }),
     ];
   }
   return ''; 
 };
+
 const renderCipherLink = () => {
   const cipherMod = getConfig().modules.cipherMod === 'on';
   if (cipherMod) {
@@ -210,38 +219,67 @@ const renderCipherLink = () => {
 };
 
 const template = (titlePrefix, ...elements) => {
+  const currentConfig = getConfig();
+    const theme = currentConfig.themes.current || "Dark-SNH";
+    const themeLink = link({
+    rel: "stylesheet",
+    href: `/assets/themes/${theme}.css`
+  });
   const nodes = html(
     { lang: "en" },
     head(
       title(titlePrefix, " | Oasis"),
-      link({ rel: "stylesheet", href: "/assets/style.css" }),
-      link({ rel: "icon", href: "/assets/favicon.svg" }),
+      link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
+      themeLink,
+      link({ rel: "icon", href: "/assets/images/favicon.svg" }),
       meta({ charset: "utf-8" }),
       meta({ name: "description", content: i18n.oasisDescription }),
       meta({ name: "viewport", content: toAttributes({ width: "device-width", "initial-scale": 1 }) })
     ),
     body(
       div(
-        { class: "header" },      
-          a(
-            {
-             class: "logo-icon",
-             href: "https://solarnethub.com",
-            },
-             img({ class: "logo-icon", src: "/assets/snh-oasis.jpg" })
-           ),
-        nav(
-          ul(
-            navLink({ href: "/profile", emoji: "⚉", text: i18n.profile }),
-            renderInboxLink(),
-            renderCipherLink(),
-            navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }),
-            renderInvitesLink(),
-            navLink({ href: "/modules", emoji: "ꗣ", text: i18n.modules }),
-            renderLegacyLink(),
-            navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }),
-            renderWalletLink()
+        { class: "header" },
+        div(
+          { class: "left-bar" },            
+          a({ class: "logo-icon", href: "https://solarnethub.com" },
+            img({ class: "logo-icon", src: "/assets/images/snh-oasis.jpg" })
+          ),
+          nav(
+            ul(
+              navLink({ href: "/profile", emoji: "⚉", text: i18n.profile }),
+              renderInboxLink(),
+              renderWalletLink(),
+              hr,
+              renderCipherLink(),
+              renderLegacyLink(),
+              hr,
+              navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }),
+              renderInvitesLink(),
+              hr,
+              navLink({ href: "/modules", emoji: "ꗣ", text: i18n.modules }),
+              navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
+            )
           )
+        ),
+        div(
+          { class: "right-bar" },
+          nav(
+            ul(
+             navLink({ href: "/publish", emoji: "❂", text: i18n.publish }),
+             navLink({ href: "/search", emoji: "ꔅ", text: i18n.search }),
+             form(
+            { action: "/search", method: "get" },
+            input({
+              name: "query",
+              required: false,
+              type: "search",
+              placeholder: i18n.searchPlaceholder,
+              class: "search-input",
+              minlength: 3
+            })
+          )
+         )
+        ),
         )
       ),
       div(
@@ -261,21 +299,13 @@ const template = (titlePrefix, ...elements) => {
             )
           )
         ),
-        main({ id: "content", class: "main-column" }, elements),
-        div(
-          { class: "sidebar-right" },
-          nav(
-            ul(
-              navLink({ href: "/publish", emoji: "❂", text: i18n.publish }),
-              navLink({ href: "/search", emoji: "ꔅ", text: i18n.search }),
-            )
-          )
-        )
+        main({ id: "content", class: "main-column" }, elements)
       )
     )
   );
   return doctypeString + nodes.outerHTML;
 };
+// menu END
 
 const thread = (messages) => {
   let lookingForTarget = true;
@@ -371,14 +401,6 @@ const postSnippet = (text) => {
   return text;
 };
 
-/**
- * Render a section containing a link that takes users to the context for a
- * thread preview.
- *
- * @param {Array} thread with SSB message objects
- * @param {Boolean} isComment true if this is shown in the context of a comment
- *  instead of a post
- */
 const continueThreadComponent = (thread, isComment) => {
   const encoded = {
     next: encodeURIComponent(thread[THREAD_PREVIEW_LENGTH + 1].key),
@@ -398,16 +420,6 @@ const continueThreadComponent = (thread, isComment) => {
   }
 };
 
-/**
- * Render an aside with a preview of comments on a message
- *
- * For posts, up to three comments are shown, for comments, up to 3 messages
- * directly following this one in the thread are displayed. If there are more
- * messages in the thread, a link is rendered that links to the rest of the
- * context.
- *
- * @param {Object} post for which to display the aside
- */
 const postAside = ({ key, value }) => {
   const thread = value.meta.thread;
   if (thread == null) return null;
@@ -652,9 +664,6 @@ exports.editProfileView = ({ name, description }) =>
     )
   );
 
-/**
- * @param {{avatarUrl: string, description: string, feedId: string, messages: any[], name: string, relationship: object, firstPost: object, lastPost: object}} input
- */
 exports.authorView = ({
   avatarUrl,
   description,
@@ -944,7 +953,11 @@ exports.threadView = ({ messages }) => {
   const rootSnippet = postSnippet(
     lodash.get(rootMessage, "value.content.text", i18n.mysteryDescription)
   );
-  return template([`@${rootAuthorName}: `, rootSnippet], thread(messages));
+  return template([`@${rootAuthorName}: `, rootSnippet], 
+    div(
+    thread(messages)
+    )
+  );
 };
 
 exports.publishView = (preview, text, contentWarning) => {
@@ -1383,20 +1396,35 @@ exports.modulesView = () => {
   );
 };
 
-exports.settingsView = ({ theme, version }) => {
+const themeFilePath = path.join(__dirname, '../configs/oasis-config.json');
+const getThemeConfig = () => {
+  try {
+    const configData = fs.readFileSync(themeFilePath);
+    return JSON.parse(configData);
+  } catch (error) {
+    console.error('Error reading config file:', error);
+    return {};
+  }
+};
+
+exports.settingsView = ({ version }) => {
+  const currentThemeConfig = getThemeConfig();
+  const theme = currentThemeConfig.themes?.current || "Dark-SNH";
     const currentConfig = getConfig();
     const walletUrl = currentConfig.wallet.url
     const walletUser = currentConfig.wallet.user
-    const walletFee = currentConfig.wallet.fee
-
+    const walletFee = currentConfig.wallet.feee;
   const themeElements = [
-    option({ value: "SNH-Oasis", selected: true }, "SNH-Oasis"),
-  ];
-
-  const languageOption = (longName, shortName, selectedLanguage) =>
-    shortName === selectedLanguage
+    option({ value: "Dark-SNH", selected: theme === "Dark-SNH" ? true : undefined }, "Dark-SNH"),
+    option({ value: "Clear-SNH", selected: theme === "Clear-SNH" ? true : undefined }, "Clear-SNH"),
+    option({ value: "Purple-SNH", selected: theme === "Purple-SNH" ? true : undefined }, "Purple-SNH"),
+    option({ value: "Matrix-SNH", selected: theme === "Matrix-SNH" ? true : undefined }, "Matrix-SNH"),
+   ];
+  const languageOption = (longName, shortName) => {
+    return shortName === selectedLanguage
       ? option({ value: shortName, selected: true }, longName)
       : option({ value: shortName }, longName);
+  };
 
   const rebuildButton = form(
     { action: "/settings/rebuild", method: "post" },
@@ -1413,7 +1441,7 @@ exports.settingsView = ({ theme, version }) => {
       h2(i18n.theme),
       p(i18n.themeIntro),
       form(
-        { action: "/theme.css", method: "post" },
+        { action: "/settings/theme", method: "post" },
         select({ name: "theme" }, ...themeElements),
         br,
         br,
@@ -1425,9 +1453,9 @@ exports.settingsView = ({ theme, version }) => {
       form(
         { action: "/language", method: "post" },
         select({ name: "language" }, [
-          languageOption("English", "en", selectedLanguage),
-          languageOption("Español", "es", selectedLanguage),
-          languageOption("Français", "fr", selectedLanguage),
+          languageOption("English", "en"),
+          languageOption("Español", "es"),
+          languageOption("Français", "fr"),
         ]),
         br,
         br,
@@ -1459,7 +1487,6 @@ exports.settingsView = ({ theme, version }) => {
   );
 };
 
-/** @param {{ viewTitle: string, viewDescription: string }} input */
 const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => {
   if (!viewTitle && !viewDescription) {
     return null;
@@ -1492,7 +1519,6 @@ const messageListView = ({
   viewTitle = null,
   viewDescription = null,
   viewElements = null,
-  // If `aside = true`, it will show a few comments in the thread.
   aside = null,
 }) => {
   return template(
@@ -1676,8 +1702,6 @@ exports.searchView = ({ messages, query }) => {
 
 const imageResult = ({ id, infos }) => {
   const encodedBlobId = encodeURIComponent(id);
-  // only rendering the first message result so far
-  // todo: render links to the others as well
   const info = infos[0];
   const encodedMsgId = encodeURIComponent(info.msg);
 
@@ -1747,9 +1771,7 @@ exports.hashtagView = ({ messages, hashtag }) => {
   );
 };
 
-/** @param {{percent: number}} input */
 exports.indexingView = ({ percent }) => {
-  // TODO: i18n
   const message = `Oasis has only processed ${percent}% of the messages and needs to catch up. This page will refresh every 10 seconds. Thanks for your patience! ❤`;
 
   const nodes = html(
@@ -1864,7 +1886,7 @@ exports.walletHistoryView = async (balance, transactions) => {
 }
 
 exports.walletReceiveView = async (balance, address) => {
-  const QRCode = require('qrcode');
+  const QRCode = require('../server/node_modules/qrcode');
   const qrImage = await QRCode.toString(address, { type: 'svg' });
   const qrContainer = address + qrImage
 

+ 12 - 31
src/views/markdown.js

@@ -1,16 +1,13 @@
 "use strict";
 
-const md = require("ssb-markdown");
-const ssbMessages = require("ssb-msgs");
-const ssbRef = require("ssb-ref");
-const { span } = require("hyperaxe");
+const md = require("../server/node_modules/ssb-markdown");
+const ssbMessages = require("../server/node_modules/ssb-msgs");
+const ssbRef = require("../server/node_modules/ssb-ref");
+const { span } = require("../server/node_modules/hyperaxe");
 
-/** @param {{ link: string}[]} mentions */
 const toUrl = (mentions) => {
-  /** @type {{name: string, link: string}[]} */
   const mentionNames = [];
 
-  /** @param {{ link: string, name: string}} arg */
   const handleLink = ({ name, link }) => {
     if (typeof name === "string") {
       const atName = name.charAt(0) === "@" ? name : `@${name}`;
@@ -20,43 +17,27 @@ const toUrl = (mentions) => {
 
   ssbMessages.links(mentions, "feed").forEach(handleLink);
 
-  /** @param {string} ref */
   const urlHandler = (ref) => {
-    // @mentions
     const found = mentionNames.find(({ name }) => name === ref);
-    if (found !== undefined) {
-      return `/author/${encodeURIComponent(found.link)}`;
-    }
+    if (found) return `/author/${encodeURIComponent(found.link)}`;
+
+    if (ssbRef.isFeedId(ref)) return `/author/${encodeURIComponent(ref)}`;
+    if (ssbRef.isMsgId(ref)) return `/thread/${encodeURIComponent(ref)}`;
 
-    if (ssbRef.isFeedId(ref)) {
-      return `/author/${encodeURIComponent(ref)}`;
-    }
-    if (ssbRef.isMsgId(ref)) {
-      return `/thread/${encodeURIComponent(ref)}`;
-    }
     const splitIndex = ref.indexOf("?");
     const blobRef = splitIndex === -1 ? ref : ref.slice(0, splitIndex);
-    // const blobParams = splitIndex !== -1 ? ref.slice(splitIndex) : "";
 
-    if (ssbRef.isBlobId(blobRef)) {
-      return `/blob/${encodeURIComponent(blobRef)}`;
-    }
-    if (ref && ref[0] === "#") {
-      return `/hashtag/${encodeURIComponent(ref.substr(1))}`;
-    }
+    if (ssbRef.isBlobId(blobRef)) return `/blob/${encodeURIComponent(blobRef)}`;
+    if (ref && ref[0] === "#") return `/hashtag/${encodeURIComponent(ref.substr(1))}`;
+
     return "";
   };
 
   return urlHandler;
 };
 
-/**
- * @param {string} input
- * @param {{name: string, link: string}[]} mentions
- */
 module.exports = (input, mentions = []) =>
   md.block(input, {
     toUrl: toUrl(mentions),
-    /** @param character {string} */
     emoji: (character) => span({ class: "emoji" }, character).outerHTML,
-  });
+  });