Browse Source

added plugin modules

psy 2 months ago
parent
commit
72ae2cfcbf
8 changed files with 554 additions and 116 deletions
  1. 2 2
      package-lock.json
  2. 1 1
      package.json
  3. 97 0
      src/assets/style.css
  4. 100 15
      src/index.js
  5. 27 0
      src/modules-config.js
  6. 11 0
      src/modules.json
  7. 43 11
      src/views/i18n.js
  8. 273 87
      src/views/index.js

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.1",
+  "version": "0.3.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@krakenslab/oasis",
-      "version": "0.3.0",
+      "version": "0.3.1",
       "hasInstallScript": true,
       "license": "AGPL-3.0",
       "dependencies": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.1",
+  "version": "0.3.2",
   "description": "SNH-Oasis Project Network GUI",
   "repository": {
     "type": "git",

+ 97 - 0
src/assets/style.css

@@ -341,6 +341,103 @@ nav > ul > li > a {
   font-weight: bold;
 }
 
+.popular-link.enabled {
+  display: block;
+}
+
+.topics-link.enabled {
+  display: block;
+}
+
+.latest-link.enabled {
+  display: block;
+}
+
+.threads-link.enabled {
+  display: block;
+}
+.multiverse-link.enabled {
+  display: block;
+}
+
+.inbox-link.enabled {
+  display: block;
+}
+
+.invites-link.enabled {
+  display: block;
+}
+
+.wallet-link.enabled {
+  display: block;
+}
+
+.module-description {
+  margin-bottom: 20px;
+  display: flex;
+  align-items: center;
+}
+
+.input-container {
+  display: flex;
+  align-items: center;
+  margin-top: 10px;
+}
+
+.module-table {
+  width: 100%;
+  border: none; 
+}
+
+.module-table td {
+  width: 80%;
+  border: none;
+}
+
+.module-table td input {
+  width: 20px;
+  text-align: right;
+  height: 20px;
+  border: none; 
+}
+
+.module-table td:nth-child(odd) {
+  width: 80px;
+  text-align: left;
+  border: none;
+}
+
+.module-table td:nth-child(even) {
+  width: 20px;
+  text-align: right; 
+  border: none;
+}
+
+.input-checkbox {
+  width: 16px; 
+  height: 16px;
+  border: 1px solid black;
+  background-color: white;
+}
+
+.input-checkbox:checked {
+  background-color: black;
+  border: 1px solid white;
+}
+
+.save-button-container {
+  text-align: left;
+  margin-top: 1rem;
+}
+
+input, textarea, select {
+  caret-color: transparent;
+}
+
+input[type="text"], textarea, select {
+  caret-color: transparent;
+}
+
 .author-action > a {
   text-decoration: underline;
 }

+ 100 - 15
src/index.js

@@ -82,6 +82,8 @@ 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('./modules-config');
+
 const oasisCheckPath = "/.well-known/oasis";
 
 process.on("uncaughtException", function (err) {
@@ -126,12 +128,6 @@ Alternatively, you can set the default port in ${defaultConfigFile} with:
   }
 });
 
-// HACK: We must get the CLI config and then delete environment variables.
-// This hides arguments from other upstream modules who might parse them.
-//
-// Unfortunately some modules think that our CLI options are meant for them,
-// and since there's no way to disable that behavior (!) we have to hide them
-// manually by setting the args property to an empty array.
 process.argv = [];
 
 const http = require("./http");
@@ -375,6 +371,7 @@ const {
   imageSearchView,
   setLanguage,
   settingsView,
+  modulesView,
   peersView,
   invitesView,
   topicsView,
@@ -442,7 +439,12 @@ router
     ctx.body = "oasis";
   })
   .get("/public/popular/:period", async (ctx) => {
-    const { period } = ctx.params;
+    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";
@@ -468,14 +470,29 @@ router
     ctx.body = await publicPopular({ period });
   })
   .get("/public/latest", async (ctx) => {
+    const latestMod = ctx.cookies.get("latestMod") || 'on';
+    if (latestMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const messages = await post.latest();
     ctx.body = await latestView({ messages });
   })
   .get("/public/latest/extended", async (ctx) => {
+    const extendedMod = ctx.cookies.get("extendedMod") || 'on';
+    if (extendedMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const messages = await post.latestExtended();
     ctx.body = await extendedView({ messages });
   })
   .get("/public/latest/topics", async (ctx) => {
+    const topicsMod = ctx.cookies.get("topicsMod") || 'on';
+    if (topicsMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const messages = await post.latestTopics();
     const channels = await post.channels();
     const list = channels.map((c) => {
@@ -485,10 +502,20 @@ router
     ctx.body = await topicsView({ messages, prefix });
   })
   .get("/public/latest/summaries", async (ctx) => {
-    const messages = await post.latestSummaries();
-    ctx.body = await summaryView({ messages });
+  const summariesMod = ctx.cookies.get("summariesMod") || 'on';
+  if (summariesMod !== 'on') {
+    ctx.redirect('/modules');
+    return;
+  }
+  const messages = await post.latestSummaries();
+  ctx.body = await summaryView({ messages });
   })
   .get("/public/latest/threads", async (ctx) => {
+    const threadsMod = ctx.cookies.get("threadsMod") || 'on';
+    if (threadsMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const messages = await post.latestThreads();
     ctx.body = await threadsView({ messages });
   })
@@ -557,19 +584,24 @@ router
     ctx.body = await imageSearchView({ blobs, query });
   })
   .get("/inbox", async (ctx) => {
-    const inbox = async () => {
+    const theme = ctx.cookies.get("theme") || config.theme;
+    const inboxMod = ctx.cookies.get("inboxMod") || 'on';
+
+    if (inboxMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const inboxMessages = async () => {
       const messages = await post.inbox();
       return privateView({ messages });
     };
-    ctx.body = await inbox();
+    ctx.body = await inboxMessages();
   })
   .get("/hashtag/:hashtag", async (ctx) => {
     const { hashtag } = ctx.params;
     const messages = await post.fromHashtag(hashtag);
-
     ctx.body = await hashtagView({ hashtag, messages });
   })
-
   .get("/theme.css", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
   
@@ -775,6 +807,19 @@ router
     };
     ctx.body = await image({ blobId, imageSize: Number(imageSize) });
   })
+  .get("/modules", async (ctx) => {
+    const configMods = getConfig();
+    const popularMod = ctx.cookies.get('popularMod', { signed: false }) || configMods.popularMod;
+    const topicsMod = ctx.cookies.get('topicsMod', { signed: false }) || configMods.topicsMod;
+    const summariesMod = ctx.cookies.get('summariesMod', { signed: false }) || configMods.summariesMod;
+    const latestMod = ctx.cookies.get('latestMod', { signed: false }) || configMods.latestMod;
+    const threadsMod = ctx.cookies.get('threadsMod', { signed: false }) || configMods.threadsMod;
+    const multiverseMod = ctx.cookies.get('multiverseMod', { signed: false }) || configMods.multiverseMod;
+    const inboxMod = ctx.cookies.get('inboxMod', { signed: false }) || configMods.inboxMod;
+    const invitesMod = ctx.cookies.get('invitesMod', { signed: false }) || configMods.invitesMod;
+    const walletMod = ctx.cookies.get('walletMod', { signed: false }) || configMods.walletMod;
+    ctx.body = modulesView({ popularMod, topicsMod, summariesMod, latestMod, threadsMod, multiverseMod, inboxMod, invitesMod, walletMod });
+  })
   .get("/settings", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
     const walletUrl = ctx.cookies.get("wallet_url") || config.walletUrl;
@@ -814,6 +859,11 @@ router
   })
   .get("/invites", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
+    const invitesMod = ctx.cookies.get("invitesMod") || 'on'; 
+    if (invitesMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const getMeta = async ({ theme }) => {
       return invitesView({});
     };
@@ -871,6 +921,11 @@ router
     ctx.body = await commentView({ messages, myFeedId, parentMessage });
   })
   .get("/wallet", async (ctx) => {
+    const walletMod = ctx.cookies.get("walletMod") || 'on';
+    if (walletMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
     const url = ctx.cookies.get("wallet_url") || config.walletUrl;
     const user = ctx.cookies.get("wallet_user") || config.walletUser;
     const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
@@ -1154,16 +1209,46 @@ router
     ctx.redirect("/invites");
   })
   .post("/settings/rebuild", async (ctx) => {
-    // Do not wait for rebuild to finish.
     meta.rebuild();
     ctx.redirect("/settings");
   })
+  .post("/save-modules", koaBody(), async (ctx) => {
+    const popularMod = ctx.request.body.popularForm === 'on' ? 'on' : 'off';
+    const topicsMod = ctx.request.body.topicsForm === 'on' ? 'on' : 'off';
+    const summariesMod = ctx.request.body.summariesForm === 'on' ? 'on' : 'off';
+    const latestMod = ctx.request.body.latestForm === 'on' ? 'on' : 'off';
+    const threadsMod = ctx.request.body.threadsForm === 'on' ? 'on' : 'off';
+    const multiverseMod = ctx.request.body.multiverseForm === 'on' ? 'on' : 'off';
+    const inboxMod = ctx.request.body.inboxForm === 'on' ? 'on' : 'off';
+    const invitesMod = ctx.request.body.invitesForm === 'on' ? 'on' : 'off';
+    const walletMod = ctx.request.body.walletForm === 'on' ? 'on' : 'off';
+    ctx.cookies.set("popularMod", popularMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("topicsMod", topicsMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("summariesMod", summariesMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("latestMod", latestMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("threadsMod", threadsMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("multiverseMod", multiverseMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("inboxMod", inboxMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("invitesMod", invitesMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    ctx.cookies.set("walletMod", walletMod, { httpOnly: true, maxAge: 86400000, path: '/' });
+    const currentConfig = getConfig();
+    currentConfig.popularMod = popularMod;
+    currentConfig.topicsMod = topicsMod;
+    currentConfig.summariesMod = summariesMod;
+    currentConfig.latestMod = latestMod;
+    currentConfig.threadsMod = threadsMod;
+    currentConfig.multiverseMod = multiverseMod;
+    currentConfig.inboxMod = inboxMod;
+    currentConfig.invitesMod = invitesMod;
+    currentConfig.walletMod = walletMod;
+    saveConfig(currentConfig);
+    ctx.redirect(`/modules`);
+  })
   .post("/settings/wallet", koaBody(), async (ctx) => {
     const url = String(ctx.request.body.wallet_url);
     const user = String(ctx.request.body.wallet_user);
     const pass = String(ctx.request.body.wallet_pass);
     const fee = String(ctx.request.body.wallet_fee);
-
     url && url.trim() !== "" && ctx.cookies.set("wallet_url", url);
     user && user.trim() !== "" && ctx.cookies.set("wallet_user", user);
     pass && pass.trim() !== "" && ctx.cookies.set("wallet_pass", pass);

+ 27 - 0
src/modules-config.js

@@ -0,0 +1,27 @@
+const fs = require('fs');
+const path = require('path');
+
+const configFilePath = path.join(__dirname, 'modules.json');
+
+if (!fs.existsSync(configFilePath)) {
+  const defaultConfig = {
+    invitesMod: 'on',
+    walletMod: 'on',
+  };
+  fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 2));
+}
+
+const getConfig = () => {
+  const configData = fs.readFileSync(configFilePath);
+  return JSON.parse(configData);
+};
+
+const saveConfig = (newConfig) => {
+  fs.writeFileSync(configFilePath, JSON.stringify(newConfig, null, 2));
+};
+
+module.exports = {
+  getConfig,
+  saveConfig,
+};
+

+ 11 - 0
src/modules.json

@@ -0,0 +1,11 @@
+{
+  "popularMod": "on",
+  "topicsMod": "on",
+  "summariesMod": "on",
+  "latestMod": "on",
+  "threadsMod": "on",
+  "multiverseMod": "on",
+  "inboxMod": "on",
+  "invitesMod": "on",
+  "walletMod": "on"
+}

+ 43 - 11
src/views/i18n.js

@@ -22,14 +22,14 @@ const i18n = {
       strong("Posts"),
       " from yourself and inhabitants you support, sorted by recency.",
     ],
-    topics: "Themes",
+    topics: "Topics",
     topicsDescription: [
-      strong("Themes"),
+      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("Themes and some comments"),
+      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",
@@ -57,7 +57,23 @@ const i18n = {
     settings: "Settings",
     continueReading: "continue reading",
     moreComments: "more comment",
-    readThread: "read the rest of the thread",
+    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",
+    saveSettings: "Save configuration",
     // post actions
     comment: "Comment",
     subtopic: "Subtopic",
@@ -135,7 +151,7 @@ const i18n = {
     updateit: "Get updates",
     info: "Info",
     settingsIntro: ({ version }) => [
-      `SNH-Oasis: [${version}]`,
+      `Oasis: [${version}]`,
     ],
     theme: "Theme",
     themeIntro:
@@ -266,14 +282,14 @@ const i18n = {
       strong("Posts"), 
       " tuyos y de habitantes que apoyas, ordenados por los más recientes.",
     ],
-    topics: "Temáticas",
+    topics: "Temas",
     topicsDescription: [
-      strong("Temáticas"),
+      strong("Temas"),
       " tuyas y de habitantes que apoyas, ordenadas por las más recientes. Selecciona la hora de publicación para leer el hilo completo.",
     ],
-    summaries: "Resumen",
+    summaries: "Resúmenes",
     summariesDescription: [
-      strong("Temáticas y algunos comentarios"),
+      strong("Temas y algunos comentarios"),
       " tuyos y de habitantes que apoyas, ordenado por lo más reciente. Selecciona la hora de publicación para leer el hilo completo.",
     ],
     threads: "Hilos",
@@ -298,7 +314,23 @@ const i18n = {
     comment: "Comentar",
     reply: "Responder",
     subtopic: "Subhilo",
-    json: "JSON",
+    json: "JSON", 
+    // modules
+    modules: "Módulos",
+    modulesViewTitle: "Módulos",
+    modulesViewDescription: "Configura tu entorno activando y desactivando módulos.",
+    inbox: "Buzón",
+    multiverse: "Multiverso",
+    popularLabel: "⌘ Destacadas",
+    topicsLabel: "ϟ Temas",
+    summariesLabel: "※ Resúmenes",
+    threadsLabel: "♺ Hilos",
+    multiverseLabel: "∞ Multiverso",
+    latestLabel: "☄ Novedades",
+    inboxLabel: "☂ Buzón",
+    invitesLabel: "ꔹ Invitaciones",
+    walletLabel: "❄ Cartera",
+    saveSettings: "Salvar configuración",
     // relationships
     relationshipNotFollowing: "",
     relationshipTheyFollow: "",
@@ -363,7 +395,7 @@ const i18n = {
     updateit: "Obtener actualizaciones",
     info: "Info",
     settingsIntro: ({ version }) => [
-      `SNH-Oasis: [${version}]`,
+      `Oasis: [${version}]`,
     ],
     theme: "Tema",
     themeIntro:

+ 273 - 87
src/views/index.js

@@ -18,8 +18,6 @@ async function checkForUpdate() {
         { action: "/update", method: "post" },
         button({ type: "submit" }, i18n.updateit)
       );
-    } else {
-      console.log("\noasis@version: no updates required.\n");
     }
   } catch (error) {
     console.error("\noasis@version: error fetching package.json:", error.message, "\n");
@@ -99,86 +97,138 @@ const toAttributes = (obj) =>
 // non-breaking space
 const nbsp = "\xa0";
 
-const template = (titlePrefix, ...elements) => {
-  const navLink = ({ href, emoji, text }) =>
-    li(
-      a(
-        { href, class: titlePrefix === text ? "current" : "" },
-        span({ class: "emoji" }, emoji),
-        nbsp,
-        text
-      )
-    );
+const { saveConfig, getConfig } = require('../modules-config');
+const configMods = getConfig();
+const navLink = ({ href, emoji, text, current }) =>
+  li(
+    a(
+      { href, class: current ? "current" : "" },
+      span({ class: "emoji" }, emoji),
+      nbsp,
+      text
+    )
+  );
 
-  const customCSS = (filename) => {
-    const customStyleFile = path.join(
-      envPaths("oasis", { suffix: "" }).config,
-      filename
-    );
-    try {
-      if (fs.existsSync(customStyleFile)) {
-        return link({ rel: "stylesheet", href: filename });
-      }
-    } catch (error) {
-      return "";
+const customCSS = (filename) => {
+  const customStyleFile = path.join(
+    envPaths("oasis", { suffix: "" }).config,
+    filename
+  );
+  try {
+    if (fs.existsSync(customStyleFile)) {
+      return link({ rel: "stylesheet", href: filename });
     }
-  };
+  } catch (error) {
+    return "";
+  }
+};
+
+const renderPopularLink = () => {
+  const popularMod = getConfig().popularMod === 'on';
+  return popularMod 
+    ? navLink({ href: "/public/popular/day", emoji: "⌘", text: i18n.popular, class: "popular-link enabled" }) 
+    : ''; 
+};
+const renderTopicsLink = () => {
+  const topicsMod = getConfig().topicsMod === 'on';
+  return topicsMod 
+    ? navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics, class: "topics-link enabled" }) 
+    : ''; 
+};
+const renderSummariesLink = () => {
+  const summariesMod = getConfig().summariesMod === 'on';
+  return summariesMod 
+    ? navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries, class: "summaries-link enabled" }) 
+    : ''; 
+};
+const renderLatestLink = () => {
+  const latestMod = getConfig().latestMod === 'on';
+  return latestMod 
+    ? navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest, class: "latest-link enabled" }) 
+    : ''; 
+};
+const renderThreadsLink = () => {
+  const threadsMod = getConfig().threadsMod === 'on';
+  return threadsMod 
+    ? navLink({ href: "/public/latest/threads", emoji: "♺", text: i18n.threads, class: "threads-link enabled" }) 
+    : ''; 
+};
+const renderMultiverseLink = () => {
+  const multiverseMod = getConfig().multiverseMod === 'on';
+  return multiverseMod 
+    ? navLink({ href: "/public/latest/extended", emoji: "∞", text: i18n.multiverse, class: "multiverse-link enabled" }) 
+    : ''; 
+};
+const renderInboxLink = () => {
+  const inboxMod = getConfig().inboxMod === 'on';
+  return inboxMod 
+    ? navLink({ href: "/inbox", emoji: "☂", text: i18n.inbox, class: "inbox-link enabled" }) 
+    : ''; 
+};
+const renderInvitesLink = () => {
+  const invitesMod = getConfig().invitesMod === 'on';
+  return invitesMod 
+    ? navLink({ href: "/invites", emoji: "ꔹ", text: i18n.invites, class: "invites-link enabled" }) 
+    : ''; 
+};
+const renderWalletLink = () => {
+  const walletMod = getConfig().walletMod === 'on';
+  if (walletMod) {
+    return [
+      navLink({ href: "/wallet", emoji: "❄", text: i18n.wallet, class: "wallet-link enabled" }),
+      hr()
+    ];
+  }
+  return ''; 
+};
 
+const template = (titlePrefix, ...elements) => {
   const nodes = html(
     { lang: "en" },
     head(
-      title(titlePrefix, " | SNH-Oasis"),
+      title(titlePrefix, " | Oasis"),
       link({ rel: "stylesheet", href: "/theme.css" }),
       link({ rel: "stylesheet", href: "/assets/style.css" }),
       link({ rel: "stylesheet", href: "/assets/highlight.css" }),
       customCSS("/custom-style.css"),
       link({ rel: "icon", type: "image/svg+xml", href: "/assets/favicon.svg" }),
       meta({ charset: "utf-8" }),
-      meta({
-        name: "description",
-        content: i18n.oasisDescription,
-      }),
-      meta({
-        name: "viewport",
-        content: toAttributes({ width: "device-width", "initial-scale": 1 }),
-      })
+      meta({ name: "description", content: i18n.oasisDescription }),
+      meta({ name: "viewport", content: toAttributes({ width: "device-width", "initial-scale": 1 }) })
     ),
     body(
       nav(
         ul(
-          //navLink({ href: "/imageSearch", emoji: "✧", text: i18n.imageSearch }),
           navLink({ href: "/mentions", emoji: "✺", text: i18n.mentions }),
-          navLink({ href: "/public/popular/day", emoji: "⌘", text: i18n.popular }),
+          renderPopularLink(),
           hr,
-          navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics }),
-          navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries }),
-          navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest }),
-          navLink({ href: "/public/latest/threads", emoji: "♺", text: i18n.threads }),
+          renderTopicsLink(),
+          renderSummariesLink(),
+          renderLatestLink(),
+          renderThreadsLink(),
           hr,
-          navLink({ href: "/public/latest/extended", emoji: "∞", text: i18n.extended }),
+          renderMultiverseLink()
         )
       ),
       main({ id: "content" }, elements),
       nav(
         ul(
-          navLink({ href: "/publish", emoji: "❂",text: i18n.publish }),
+          navLink({ href: "/publish", emoji: "❂", text: i18n.publish }),
+          renderInboxLink(),
           navLink({ href: "/search", emoji: "✦", text: i18n.search }),
-          navLink({ href: "/inbox", emoji: "☂", text: i18n.private }),
           hr,
+          renderWalletLink(),
           navLink({ href: "/profile", emoji: "⚉", text: i18n.profile }),
-          navLink({ href: "/wallet", emoji: "❄", text: i18n.wallet }),
-          navLink({ href: "/invites", emoji: "ꔹ", text: i18n.invites }),
           navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }),
           hr,
-          navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
+          navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }),
+          navLink({ href: "/modules", emoji: "ꗣ", text: i18n.modules }),
+          renderInvitesLink(),
         )
-      )
+      ),
     )
   );
-
-  const result = doctypeString + nodes.outerHTML;
-
-  return result;
+  return doctypeString + nodes.outerHTML;
 };
 
 const thread = (messages) => {
@@ -1095,44 +1145,50 @@ exports.peersView = async ({ peers, supports, blocks, recommends }) => {
     );
 };
 
-exports.invitesView = ({ invites }) => {
-  try{
-    var pubs = fs.readFileSync(gossipPath, "utf8");
-  }catch{
-      var pubs = undefined;
+exports.invitesView = ({ invitesEnabled }) => {
+  let pubs = [];
+  let pubsValue = "false";
+
+  try {
+    pubs = fs.readFileSync(gossipPath, "utf8");
+  } catch (error) {
+    pubs = undefined;
   }
-  if (pubs == undefined) {
-        var pubsValue = "false";
-  }else{
-        var keys = Object.keys(pubs);
-        if (keys[0] === undefined){
-          var pubsValue = "false";
-        }else{
-          var pubsValue = "true";
-        }
+
+  if (pubs) {
+    try {
+      pubs = JSON.parse(pubs);
+      if (pubs && pubs.length > 0) {
+        pubsValue = "true";
+      } else {
+        pubsValue = "false";
+      }
+    } catch (error) {
+      pubsValue = "false";
+    }
   }
+
+  let pub = [];
   if (pubsValue === "true") {
-    var pubs = JSON.parse(pubs);
-    const arr2 = [];
-    const arr3 = [];
-    for(var i=0; i<pubs.length; i++){
-      arr2.push(
-        li("PUB: " + pubs[i].host, br, 
-            i18n.inhabitants + ": " + pubs[i].announcers, br, 
-            a(
-             { href: `/author/${encodeURIComponent(pubs[i].key)}` }, 
-              pubs[i].key
-             ), br, br
-        )               
+    const arr2 = pubs.map(pubItem => {
+      return li(
+        `PUB: ${pubItem.host}`,
+        br,
+        `${i18n.inhabitants}: ${pubItem.announcers}`,
+        br,
+        a(
+          { href: `/author/${encodeURIComponent(pubItem.key)}` },
+          pubItem.key
+        ),
+        br,
+        br
       );
-    }
-    var pub = arr2;
-  }else{
-    var pub = [];
+    });
+    pub = arr2;
   }
 
- return template(
-  i18n.invites,
+  return template(
+    i18n.invites,
     section(
       { class: "message" },
       h1(i18n.invites),
@@ -1142,10 +1198,140 @@ exports.invitesView = ({ invites }) => {
         input({ name: "invite", type: "text", autofocus: true, required: true }),
         button({ type: "submit" }, i18n.acceptInvite),
         h1(i18n.acceptedInvites, " (", pub.length, ")"),
-        pub.length > 0 ? ul(pub): i18n.noInvites,
-      ),
-     )
-   );
+        pub.length > 0 ? ul(pub) : i18n.noInvites
+      )
+    )
+  );
+};
+ 
+exports.modulesView = () => {
+  const config = getConfig();
+  const popularMod = config.popularMod === 'on' ? 'on' : 'off';
+  const topicsMod = config.topicsMod === 'on' ? 'on' : 'off';
+  const summariesMod = config.summariesMod === 'on' ? 'on' : 'off';
+  const latestMod = config.latestMod === 'on' ? 'on' : 'off';
+  const threadsMod = config.threadsMod === 'on' ? 'on' : 'off';
+  const multiverseMod = config.multiverseMod === 'on' ? 'on' : 'off';
+  const inboxMod = config.inboxMod === 'on' ? 'on' : 'off';
+  const invitesMod = config.invitesMod === 'on' ? 'on' : 'off';
+  const walletMod = config.walletMod === 'on' ? 'on' : 'off';
+  
+  return template(
+    i18n.modulesView,
+    section(
+      { class: "modules-view" },
+      h1(i18n.modulesViewTitle),
+      p(i18n.modulesViewDescription)
+    ),
+    section(
+      form(
+        { action: "/save-modules", method: "post" },
+        table(
+          { class: "module-table" },
+          tr(
+            td(i18n.popularLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "popularMod",
+                name: "popularForm",
+                class: "input-checkbox",
+                checked: popularMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.latestLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "latestMod",
+                name: "latestForm",
+                class: "input-checkbox",
+                checked: latestMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.walletLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "walletMod",
+                name: "walletForm",
+                class: "input-checkbox",
+                checked: walletMod === 'on' ? true : undefined
+              })
+            )
+          ),
+          tr(
+            td(i18n.topicsLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "topicsMod",
+                name: "topicsForm",
+                class: "input-checkbox",
+                checked: topicsMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.threadsLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "threadsMod",
+                name: "threadsForm",
+                class: "input-checkbox",
+                checked: threadsMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.inboxLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "inboxMod",
+                name: "inboxForm",
+                class: "input-checkbox",
+                checked: inboxMod === 'on' ? true : undefined
+              })
+            )
+          ),
+          tr(
+            td(i18n.summariesLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "summariesMod",
+                name: "summariesForm",
+                class: "input-checkbox",
+                checked: summariesMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.multiverseLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "multiverseMod",
+                name: "multiverseForm",
+                class: "input-checkbox",
+                checked: multiverseMod === 'on' ? true : undefined
+              })
+            ),
+            td(i18n.invitesLabel),
+            td(
+              input({
+                type: "checkbox",
+                id: "invitesMod",
+                name: "invitesForm",
+                class: "input-checkbox",
+                checked: invitesMod === 'on' ? true : undefined
+              })
+            )
+          )
+        ),
+        div(
+          { class: "save-button-container" },
+          button({ type: "submit", class: "submit-button" }, i18n.saveSettings)
+        )
+      )
+    )
+  );
 };
 
 exports.settingsView = ({ theme, themeNames, version, walletUrl, walletUser, walletFee }) => {
@@ -1689,7 +1875,7 @@ exports.walletSendFormView = async (balance, destination, amount, fee, statusMes
       form(
         { action: '/wallet/send', method: 'POST' },
         label({ for: 'destination' }, i18n.walletAddress),
-        input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ELH8RJGy3s7sCPqQUyN8on2gD5Fdn3BpyC', value: destination }),
+        input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX', value: destination }),
         label({ for: 'amount' }, i18n.walletAmount),
         input({ type: 'text', id: 'amount', name: 'amount', placeholder: '0.25', value: amount }),
         label({ for: 'fee' }, i18n.walletFee),