main_views.js 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525
  1. "use strict";
  2. const path = require("path");
  3. const fs = require("fs");
  4. const envPaths = require("../server/node_modules/env-paths");
  5. const debug = require("../server/node_modules/debug")("oasis");
  6. const highlightJs = require("../server/node_modules/highlight.js");
  7. const prettyMs = require("../server/node_modules/pretty-ms");
  8. const moment = require('../server/node_modules/moment');
  9. const { renderUrl } = require('../backend/renderUrl');
  10. const ssbClientGUI = require("../client/gui");
  11. const config = require("../server/ssb_config");
  12. const cooler = ssbClientGUI({ offline: config.offline });
  13. let ssb, userId;
  14. const getUserId = async () => {
  15. if (!ssb) ssb = await cooler.open();
  16. if (!userId) userId = ssb.id;
  17. return userId;
  18. };
  19. const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");
  20. const lodash = require("../server/node_modules/lodash");
  21. const markdown = require("./markdown");
  22. // set language
  23. const i18nBase = require("../client/assets/translations/i18n");
  24. let selectedLanguage = "en";
  25. let i18n = {};
  26. Object.assign(i18n, i18nBase[selectedLanguage]);
  27. exports.setLanguage = (language) => {
  28. selectedLanguage = language;
  29. const newLang = i18nBase[selectedLanguage] || i18nBase['en'];
  30. Object.keys(i18n).forEach(k => delete i18n[k]);
  31. Object.assign(i18n, newLang);
  32. };
  33. exports.i18n = i18n;
  34. exports.selectedLanguage = selectedLanguage;
  35. // markdown
  36. const markdownUrl = "https://commonmark.org/help/";
  37. const doctypeString = "<!DOCTYPE html>";
  38. const THREAD_PREVIEW_LENGTH = 3;
  39. const toAttributes = (obj) =>
  40. Object.entries(obj)
  41. .map(([key, val]) => `${key}=${val}`)
  42. .join(", ");
  43. const nbsp = "\xa0";
  44. const { getConfig } = require('../configs/config-manager.js');
  45. // menu INIT
  46. const navLink = ({ href, emoji, text, current, class: extraClass }) =>
  47. li(
  48. a(
  49. {
  50. href,
  51. class: [current ? "current" : "", extraClass]
  52. .filter(Boolean)
  53. .join(" ")
  54. },
  55. span({ class: "emoji" }, emoji),
  56. nbsp,
  57. text
  58. )
  59. );
  60. const customCSS = (filename) => {
  61. const customStyleFile = path.join(
  62. envPaths("oasis", { suffix: "" }).config,
  63. filename
  64. );
  65. try {
  66. if (fs.existsSync(customStyleFile)) {
  67. return link({ rel: "stylesheet", href: filename });
  68. }
  69. } catch (error) {
  70. return "";
  71. }
  72. };
  73. const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) =>
  74. li(
  75. { class: "oasis-nav-group" },
  76. input({
  77. type: "checkbox",
  78. id: `oasis-nav-group-${id}`,
  79. class: "oasis-nav-toggle",
  80. ...(defaultOpen ? { checked: true } : {})
  81. }),
  82. label(
  83. { for: `oasis-nav-group-${id}`, class: "oasis-nav-header" },
  84. span({ class: "emoji" }, emoji),
  85. nbsp,
  86. title,
  87. span({ class: "oasis-nav-arrow" }, "▾")
  88. ),
  89. ul({ class: "oasis-nav-list" }, ...items)
  90. );
  91. const renderPopularLink = () => {
  92. const popularMod = getConfig().modules.popularMod === "on";
  93. return popularMod
  94. ? navLink({
  95. href: "/public/popular/day",
  96. emoji: "⌘",
  97. text: i18n.popular,
  98. class: "popular-link enabled"
  99. })
  100. : "";
  101. };
  102. const renderTopicsLink = () => {
  103. const topicsMod = getConfig().modules.topicsMod === "on";
  104. return topicsMod
  105. ? navLink({
  106. href: "/public/latest/topics",
  107. emoji: "ϟ",
  108. text: i18n.topics,
  109. class: "topics-link enabled"
  110. })
  111. : "";
  112. };
  113. const renderSummariesLink = () => {
  114. const summariesMod = getConfig().modules.summariesMod === "on";
  115. if (summariesMod) {
  116. return [
  117. navLink({
  118. href: "/public/latest/summaries",
  119. emoji: "※",
  120. text: i18n.summaries,
  121. class: "summaries-link enabled"
  122. })
  123. ];
  124. }
  125. return "";
  126. };
  127. const renderLatestLink = () => {
  128. const latestMod = getConfig().modules.latestMod === "on";
  129. return latestMod
  130. ? navLink({
  131. href: "/public/latest",
  132. emoji: "☄",
  133. text: i18n.latest,
  134. class: "latest-link enabled"
  135. })
  136. : "";
  137. };
  138. const renderThreadsLink = () => {
  139. const threadsMod = getConfig().modules.threadsMod === "on";
  140. if (threadsMod) {
  141. return [
  142. navLink({
  143. href: "/public/latest/threads",
  144. emoji: "♺",
  145. text: i18n.threads,
  146. class: "threads-link enabled"
  147. })
  148. ];
  149. }
  150. return "";
  151. };
  152. const renderInvitesLink = () => {
  153. const invitesMod = getConfig().modules.invitesMod === "on";
  154. return invitesMod
  155. ? navLink({
  156. href: "/invites",
  157. emoji: "ꔹ",
  158. text: i18n.invites,
  159. class: "invites-link enabled"
  160. })
  161. : "";
  162. };
  163. const renderWalletLink = () => {
  164. const walletMod = getConfig().modules.walletMod === "on";
  165. if (walletMod) {
  166. return [
  167. navLink({
  168. href: "/wallet",
  169. emoji: "❄",
  170. text: i18n.wallet,
  171. class: "wallet-link enabled"
  172. })
  173. ];
  174. }
  175. return "";
  176. };
  177. const renderLegacyLink = () => {
  178. const legacyMod = getConfig().modules.legacyMod === "on";
  179. if (legacyMod) {
  180. return [
  181. navLink({
  182. href: "/legacy",
  183. emoji: "ꖤ",
  184. text: i18n.legacy,
  185. class: "legacy-link enabled"
  186. })
  187. ];
  188. }
  189. return "";
  190. };
  191. const renderCipherLink = () => {
  192. const cipherMod = getConfig().modules.cipherMod === "on";
  193. if (cipherMod) {
  194. return [
  195. navLink({
  196. href: "/cipher",
  197. emoji: "ꗄ",
  198. text: i18n.cipher,
  199. class: "cipher-link enabled"
  200. })
  201. ];
  202. }
  203. return "";
  204. };
  205. const renderBookmarksLink = () => {
  206. const bookmarksMod = getConfig().modules.bookmarksMod === "on";
  207. return bookmarksMod
  208. ? navLink({
  209. href: "/bookmarks",
  210. emoji: "ꔪ",
  211. text: i18n.bookmarksLabel,
  212. class: "bookmark-link enabled"
  213. })
  214. : "";
  215. };
  216. const renderImagesLink = () => {
  217. const imagesMod = getConfig().modules.imagesMod === "on";
  218. if (imagesMod) {
  219. return [
  220. navLink({
  221. href: "/images",
  222. emoji: "ꕥ",
  223. text: i18n.imagesLabel,
  224. class: "images-link enabled"
  225. })
  226. ];
  227. }
  228. return "";
  229. };
  230. const renderVideosLink = () => {
  231. const videosMod = getConfig().modules.videosMod === "on";
  232. if (videosMod) {
  233. return [
  234. navLink({
  235. href: "/videos",
  236. emoji: "ꗟ",
  237. text: i18n.videosLabel,
  238. class: "videos-link enabled"
  239. })
  240. ];
  241. }
  242. return "";
  243. };
  244. const renderAudiosLink = () => {
  245. const audiosMod = getConfig().modules.audiosMod === "on";
  246. if (audiosMod) {
  247. return [
  248. navLink({
  249. href: "/audios",
  250. emoji: "ꔿ",
  251. text: i18n.audiosLabel,
  252. class: "audios-link enabled"
  253. })
  254. ];
  255. }
  256. return "";
  257. };
  258. const renderDocsLink = () => {
  259. const docsMod = getConfig().modules.docsMod === "on";
  260. if (docsMod) {
  261. return [
  262. navLink({
  263. href: "/documents",
  264. emoji: "ꕨ",
  265. text: i18n.docsLabel,
  266. class: "docs-link enabled"
  267. })
  268. ];
  269. }
  270. return "";
  271. };
  272. const renderTagsLink = () => {
  273. const tagsMod = getConfig().modules.tagsMod === "on";
  274. return tagsMod
  275. ? [
  276. navLink({
  277. href: "/tags",
  278. emoji: "ꖶ",
  279. text: i18n.tagsLabel,
  280. class: "tags-link enabled"
  281. })
  282. ]
  283. : "";
  284. };
  285. const renderMultiverseLink = () => {
  286. const multiverseMod = getConfig().modules.multiverseMod === "on";
  287. return multiverseMod
  288. ? navLink({
  289. href: "/public/latest/extended",
  290. emoji: "∞",
  291. text: i18n.multiverse,
  292. class: "multiverse-link enabled"
  293. })
  294. : "";
  295. };
  296. const renderMarketLink = () => {
  297. const marketMod = getConfig().modules.marketMod === "on";
  298. return marketMod
  299. ? [
  300. navLink({
  301. href: "/market",
  302. emoji: "ꕻ",
  303. text: i18n.marketTitle
  304. })
  305. ]
  306. : "";
  307. };
  308. const renderJobsLink = () => {
  309. const jobsMod = getConfig().modules.jobsMod === "on";
  310. return jobsMod
  311. ? [
  312. navLink({
  313. href: "/jobs",
  314. emoji: "ꗒ",
  315. text: i18n.jobsTitle
  316. })
  317. ]
  318. : "";
  319. };
  320. const renderProjectsLink = () => {
  321. const projectsMod = getConfig().modules.projectsMod === "on";
  322. return projectsMod
  323. ? [
  324. navLink({
  325. href: "/projects",
  326. emoji: "ꕧ",
  327. text: i18n.projectsTitle
  328. })
  329. ]
  330. : "";
  331. };
  332. const renderBankingLink = () => {
  333. const bankingMod = getConfig().modules.bankingMod === "on";
  334. return bankingMod
  335. ? navLink({
  336. href: "/banking",
  337. emoji: "ꗴ",
  338. text: i18n.bankingTitle
  339. })
  340. : "";
  341. };
  342. const renderTribesLink = () => {
  343. const tribesMod = getConfig().modules.tribesMod === "on";
  344. return tribesMod
  345. ? [
  346. navLink({
  347. href: "/tribes",
  348. emoji: "ꖥ",
  349. text: i18n.tribesTitle,
  350. class: "tribes-link enabled"
  351. })
  352. ]
  353. : "";
  354. };
  355. const renderParliamentLink = () => {
  356. const parliamentMod = getConfig().modules.parliamentMod === "on";
  357. return parliamentMod
  358. ? [
  359. navLink({
  360. href: "/parliament",
  361. emoji: "ꗞ",
  362. text: i18n.parliamentTitle,
  363. class: "parliament-link enabled"
  364. })
  365. ]
  366. : "";
  367. };
  368. const renderCourtsLink = () => {
  369. const courtsMod = getConfig().modules.courtsMod === "on";
  370. return courtsMod
  371. ? navLink({
  372. href: "/courts",
  373. emoji: "ꖻ",
  374. text: i18n.courtsTitle,
  375. class: "courts-link enabled"
  376. })
  377. : "";
  378. };
  379. const renderVotationsLink = () => {
  380. const votesMod = getConfig().modules.votesMod === "on";
  381. return votesMod
  382. ? [
  383. navLink({
  384. href: "/votes",
  385. emoji: "ꔰ",
  386. text: i18n.votationsTitle,
  387. class: "votations-link enabled"
  388. })
  389. ]
  390. : "";
  391. };
  392. const renderTrendingLink = () => {
  393. const trendingMod = getConfig().modules.trendingMod === "on";
  394. return trendingMod
  395. ? [
  396. navLink({
  397. href: "/trending",
  398. emoji: "ꗝ",
  399. text: i18n.trendingLabel,
  400. class: "trending-link enabled"
  401. })
  402. ]
  403. : "";
  404. };
  405. const renderReportsLink = () => {
  406. const reportsMod = getConfig().modules.reportsMod === "on";
  407. return reportsMod
  408. ? [
  409. navLink({
  410. href: "/reports",
  411. emoji: "ꕥ",
  412. text: i18n.reportsTitle,
  413. class: "reports-link enabled"
  414. })
  415. ]
  416. : "";
  417. };
  418. const renderOpinionsLink = () => {
  419. const opinionsMod = getConfig().modules.opinionsMod === "on";
  420. return opinionsMod
  421. ? [
  422. navLink({
  423. href: "/opinions",
  424. emoji: "ꔍ",
  425. text: i18n.opinionsTitle,
  426. class: "opinions-link enabled"
  427. })
  428. ]
  429. : "";
  430. };
  431. const renderTransfersLink = () => {
  432. const transfersMod = getConfig().modules.transfersMod === "on";
  433. return transfersMod
  434. ? [
  435. navLink({
  436. href: "/transfers",
  437. emoji: "ꘉ",
  438. text: i18n.transfersTitle,
  439. class: "transfers-link enabled"
  440. })
  441. ]
  442. : "";
  443. };
  444. const renderFeedLink = () => {
  445. const feedMod = getConfig().modules.feedMod === "on";
  446. return feedMod
  447. ? navLink({
  448. href: "/feed",
  449. emoji: "ꕿ",
  450. text: i18n.feedTitle,
  451. class: "feed-link enabled"
  452. })
  453. : "";
  454. };
  455. const renderPixeliaLink = () => {
  456. const pixeliaMod = getConfig().modules.pixeliaMod === "on";
  457. return pixeliaMod
  458. ? [
  459. navLink({
  460. href: "/pixelia",
  461. emoji: "ꔘ",
  462. text: i18n.pixeliaTitle,
  463. class: "pixelia-link enabled"
  464. })
  465. ]
  466. : "";
  467. };
  468. const renderForumLink = () => {
  469. const forumMod = getConfig().modules.forumMod === "on";
  470. return forumMod
  471. ? [
  472. navLink({
  473. href: "/forum",
  474. emoji: "ꕒ",
  475. text: i18n.forumTitle,
  476. class: "forum-link enabled"
  477. })
  478. ]
  479. : "";
  480. };
  481. const renderAgendaLink = () => {
  482. const agendaMod = getConfig().modules.agendaMod === "on";
  483. return agendaMod
  484. ? [
  485. navLink({
  486. href: "/agenda",
  487. emoji: "ꗤ",
  488. text: i18n.agendaTitle,
  489. class: "agenda-link enabled"
  490. })
  491. ]
  492. : "";
  493. };
  494. const renderAILink = () => {
  495. const aiMod = getConfig().modules.aiMod === "on";
  496. return aiMod
  497. ? [
  498. navLink({
  499. href: "/ai",
  500. emoji: "ꘜ",
  501. text: i18n.ai,
  502. class: "ai-link enabled"
  503. })
  504. ]
  505. : "";
  506. };
  507. const renderEventsLink = () => {
  508. const eventsMod = getConfig().modules.eventsMod === "on";
  509. return eventsMod
  510. ? [
  511. navLink({
  512. href: "/events",
  513. emoji: "ꕆ",
  514. text: i18n.eventsLabel,
  515. class: "events-link enabled"
  516. })
  517. ]
  518. : "";
  519. };
  520. const renderTasksLink = () => {
  521. const tasksMod = getConfig().modules.tasksMod === "on";
  522. return tasksMod
  523. ? [
  524. navLink({
  525. href: "/tasks",
  526. emoji: "ꖧ",
  527. text: i18n.tasksTitle,
  528. class: "tasks-link enabled"
  529. })
  530. ]
  531. : "";
  532. };
  533. const template = (titlePrefix, ...elements) => {
  534. const currentConfig = getConfig();
  535. const theme = currentConfig.themes.current || "Dark-SNH";
  536. const themeLink = link({
  537. rel: "stylesheet",
  538. href: `/assets/themes/${theme}.css`
  539. });
  540. const nodes = html(
  541. { lang: "en" },
  542. head(
  543. title(titlePrefix, " | Oasis"),
  544. link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
  545. themeLink,
  546. link({ rel: "icon", href: "/assets/images/favicon.svg" }),
  547. meta({ charset: "utf-8" }),
  548. meta({ name: "description", content: i18n.oasisDescription }),
  549. meta({
  550. name: "viewport",
  551. content: toAttributes({
  552. width: "device-width",
  553. "initial-scale": 1
  554. })
  555. })
  556. ),
  557. body(
  558. div(
  559. { class: "header" },
  560. div(
  561. { class: "top-bar-left" },
  562. a(
  563. { class: "logo-icon", href: "/" },
  564. img({
  565. class: "logo-icon",
  566. src: "/assets/images/snh-oasis.jpg",
  567. alt: "Oasis Logo"
  568. })
  569. ),
  570. nav(
  571. ul(
  572. navLink({
  573. href: "/inbox",
  574. emoji: "☂",
  575. text: i18n.inbox
  576. }),
  577. navLink({
  578. href: "/pm",
  579. emoji: "ꕕ",
  580. text: i18n.privateMessage
  581. }),
  582. navLink({ href: "/publish", emoji: "❂", text: i18n.publish })
  583. )
  584. )
  585. ),
  586. div(
  587. { class: "top-bar-right" },
  588. nav(
  589. ul(
  590. renderTagsLink(),
  591. navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle })
  592. )
  593. )
  594. )
  595. ),
  596. div(
  597. { class: "main-content" },
  598. div(
  599. { class: "sidebar-left" },
  600. nav(
  601. ul(
  602. navGroup(
  603. {
  604. id: "personal",
  605. emoji: "⚉",
  606. title: i18n.menuPersonal
  607. },
  608. navLink({
  609. href: "/profile",
  610. emoji: "⚉",
  611. text: i18n.profile
  612. }),
  613. navLink({
  614. href: "/cv",
  615. emoji: "ꕛ",
  616. text: i18n.cvTitle
  617. }),
  618. renderAgendaLink(),
  619. renderWalletLink(),
  620. navLink({
  621. href: "/modules",
  622. emoji: "ꗣ",
  623. text: i18n.modules
  624. }),
  625. navLink({
  626. href: "/settings",
  627. emoji: "⚙",
  628. text: i18n.settings
  629. })
  630. ),
  631. navGroup(
  632. {
  633. id: "content",
  634. emoji: "✦",
  635. title: i18n.menuContent
  636. },
  637. navLink({
  638. href: "/mentions",
  639. emoji: "✺",
  640. text: i18n.mentions
  641. }),
  642. renderLatestLink(),
  643. renderThreadsLink(),
  644. renderTopicsLink(),
  645. renderSummariesLink(),
  646. renderPopularLink(),
  647. renderMultiverseLink()
  648. ),
  649. navGroup(
  650. {
  651. id: "governance",
  652. emoji: "⚖",
  653. title: i18n.menuGovernance
  654. },
  655. navLink({
  656. href: "/inhabitants",
  657. emoji: "ꖘ",
  658. text: i18n.inhabitantsLabel
  659. }),
  660. renderTribesLink(),
  661. renderParliamentLink(),
  662. renderCourtsLink()
  663. ),
  664. navGroup(
  665. {
  666. id: "office",
  667. emoji: "⌂",
  668. title: i18n.menuOffice
  669. },
  670. renderVotationsLink(),
  671. renderEventsLink(),
  672. renderTasksLink(),
  673. renderReportsLink()
  674. ),
  675. navGroup(
  676. {
  677. id: "tools",
  678. emoji: "⚒",
  679. title: i18n.menuTools
  680. },
  681. renderAILink(),
  682. navLink({
  683. href: "/stats",
  684. emoji: "ꕷ",
  685. text: i18n.statistics
  686. }),
  687. navLink({
  688. href: "/blockexplorer",
  689. emoji: "ꖸ",
  690. text: i18n.blockchain
  691. }),
  692. renderCipherLink(),
  693. renderLegacyLink()
  694. )
  695. )
  696. )
  697. ),
  698. main({ id: "content", class: "main-column" }, elements),
  699. div(
  700. { class: "sidebar-right" },
  701. nav(
  702. ul(
  703. navGroup(
  704. {
  705. id: "network",
  706. emoji: "☍",
  707. title: i18n.menuNetwork
  708. },
  709. navLink({
  710. href: "/activity",
  711. emoji: "ꔙ",
  712. text: i18n.activityTitle
  713. }),
  714. renderTrendingLink(),
  715. renderOpinionsLink(),
  716. renderForumLink(),
  717. renderInvitesLink(),
  718. navLink({
  719. href: "/peers",
  720. emoji: "⧖",
  721. text: i18n.peers
  722. })
  723. ),
  724. navGroup(
  725. {
  726. id: "creative",
  727. emoji: "✎",
  728. title: i18n.menuCreative
  729. },
  730. renderFeedLink(),
  731. renderPixeliaLink()
  732. ),
  733. navGroup(
  734. {
  735. id: "economy",
  736. emoji: "¤",
  737. title: i18n.menuEconomy
  738. },
  739. renderBankingLink(),
  740. renderMarketLink(),
  741. renderProjectsLink(),
  742. renderJobsLink(),
  743. renderTransfersLink()
  744. ),
  745. navGroup(
  746. {
  747. id: "media",
  748. emoji: "▤",
  749. title: i18n.menuMedia
  750. },
  751. renderBookmarksLink(),
  752. renderImagesLink(),
  753. renderVideosLink(),
  754. renderAudiosLink(),
  755. renderDocsLink()
  756. )
  757. )
  758. )
  759. )
  760. )
  761. )
  762. );
  763. return doctypeString + nodes.outerHTML;
  764. };
  765. // menu END
  766. exports.template = template;
  767. const thread = (messages) => {
  768. let lookingForTarget = true;
  769. let shallowest = Infinity;
  770. for (let i = messages.length - 1; i >= 0; i--) {
  771. const msg = messages[i];
  772. const depth = lodash.get(msg, "value.meta.thread.depth", 0);
  773. if (lookingForTarget) {
  774. const isThreadTarget = Boolean(
  775. lodash.get(msg, "value.meta.thread.target", false)
  776. );
  777. if (isThreadTarget) {
  778. lookingForTarget = false;
  779. }
  780. } else {
  781. if (depth < shallowest) {
  782. lodash.set(msg, "value.meta.thread.ancestorOfTarget", true);
  783. shallowest = depth;
  784. }
  785. }
  786. }
  787. const msgList = [];
  788. for (let i = 0; i < messages.length; i++) {
  789. const j = i + 1;
  790. const currentMsg = messages[i];
  791. const nextMsg = messages[j];
  792. const depth = (msg) => {
  793. if (msg === undefined) return 0;
  794. return lodash.get(msg, "value.meta.thread.depth", 0);
  795. };
  796. msgList.push(post({ msg: currentMsg }));
  797. if (depth(currentMsg) < depth(nextMsg)) {
  798. const isAncestor = Boolean(
  799. lodash.get(currentMsg, "value.meta.thread.ancestorOfTarget", false)
  800. );
  801. const isBlocked = Boolean(nextMsg.value.meta.blocking);
  802. const nextAuthor = lodash.get(nextMsg, "value.meta.author.name") || (typeof nextMsg?.value?.author === "string" ? (nextMsg.value.author.startsWith("@") ? nextMsg.value.author.slice(1) : nextMsg.value.author) : "Anonymous");
  803. const nextSnippet = postSnippet(
  804. lodash.has(nextMsg, "value.content.contentWarning")
  805. ? lodash.get(nextMsg, "value.content.contentWarning")
  806. : lodash.get(nextMsg, "value.content.text")
  807. );
  808. msgList.push(
  809. details(
  810. isAncestor ? { open: true } : {},
  811. summary(
  812. isBlocked
  813. ? i18n.relationshipBlockingPost
  814. : `${nextAuthor}: ${nextSnippet}`
  815. )
  816. )
  817. );
  818. } else if (depth(currentMsg) > depth(nextMsg)) {
  819. const diffDepth = depth(currentMsg) - depth(nextMsg);
  820. }
  821. }
  822. return div({ class: "thread-container" }, ...msgList);
  823. };
  824. const postSnippet = (text) => {
  825. const max = 40;
  826. text = text.trim().split("\n", 3).join("\n");
  827. text = text.replace(/_|`|\*|#|^\[@.*?]|\[|]|\(\S*?\)/g, "").trim();
  828. text = text.replace(/:$/, "");
  829. text = text.trim().split("\n", 1)[0].trim();
  830. if (text.length > max) {
  831. text = text.substring(0, max - 1) + "…";
  832. }
  833. return text;
  834. };
  835. const continueThreadComponent = (thread, isComment) => {
  836. const encoded = {
  837. next: encodeURIComponent(thread[THREAD_PREVIEW_LENGTH + 1].key),
  838. parent: encodeURIComponent(thread[0].key),
  839. };
  840. const left = thread.length - (THREAD_PREVIEW_LENGTH + 1);
  841. let continueLink;
  842. if (isComment == false) {
  843. continueLink = `/thread/${encoded.parent}#${encoded.next}`;
  844. return a(
  845. { href: continueLink },
  846. i18n.continueReading, ` ${left} `, i18n.moreComments+`${left === 1 ? "" : "s"}`
  847. );
  848. } else {
  849. continueLink = `/thread/${encoded.parent}`;
  850. return a({ href: continueLink }, i18n.readThread);
  851. }
  852. };
  853. const postAside = ({ key, value }) => {
  854. const thread = value.meta.thread;
  855. if (thread == null) return null;
  856. const isComment = value.meta.postType === "comment";
  857. let postsToShow;
  858. if (isComment) {
  859. const commentPosition = thread.findIndex((msg) => msg.key === key);
  860. postsToShow = thread.slice(
  861. commentPosition + 1,
  862. Math.min(commentPosition + (THREAD_PREVIEW_LENGTH + 1), thread.length)
  863. );
  864. } else {
  865. postsToShow = thread.slice(
  866. 1,
  867. Math.min(thread.length, THREAD_PREVIEW_LENGTH + 1)
  868. );
  869. }
  870. const fragments = postsToShow.map((p) => post({ msg: p }));
  871. if (thread.length > THREAD_PREVIEW_LENGTH + 1) {
  872. fragments.push(section(continueThreadComponent(thread, isComment)));
  873. }
  874. return fragments;
  875. };
  876. const post = ({ msg, aside = false, preview = false }) => {
  877. const encoded = {
  878. key: encodeURIComponent(msg.key),
  879. author: encodeURIComponent(msg.value?.author),
  880. parent: encodeURIComponent(msg.value?.content?.root),
  881. };
  882. const url = {
  883. author: `/author/${encoded.author}`,
  884. likeForm: `/like/${encoded.key}`,
  885. link: `/thread/${encoded.key}#${encoded.key}`,
  886. parent: `/thread/${encoded.parent}#${encoded.parent}`,
  887. avatar: msg.value?.meta?.author?.avatar?.url || '/assets/images/default-avatar.png',
  888. json: `/json/${encoded.key}`,
  889. subtopic: `/subtopic/${encoded.key}`,
  890. comment: `/comment/${encoded.key}`,
  891. };
  892. const isPrivate = Boolean(msg.value?.meta?.private);
  893. const isBlocked = Boolean(msg.value?.meta?.blocking);
  894. const isRoot = msg.value?.content?.root == null;
  895. const isFork = msg.value?.meta?.postType === "subtopic";
  896. const hasContentWarning = typeof msg.value?.content?.contentWarning === "string";
  897. const isThreadTarget = Boolean(lodash.get(msg, "value.meta.thread.target", false));
  898. const authorIdForName = msg.value?.author;
  899. const name =
  900. msg.value?.meta?.author?.name ||
  901. (typeof authorIdForName === "string"
  902. ? (authorIdForName.startsWith("@") ? authorIdForName.slice(1) : authorIdForName)
  903. : "Anonymous");
  904. const content = msg.value?.content || {};
  905. const contentType = String(content.type || "");
  906. const THREAD_ENTITY_TYPES = new Set([
  907. 'bookmark',
  908. 'image',
  909. 'audio',
  910. 'video',
  911. 'document',
  912. 'votes',
  913. 'event',
  914. 'task',
  915. 'report',
  916. 'market',
  917. 'project',
  918. 'job'
  919. ]);
  920. const safeUpper = (s) => String(s || '').toUpperCase();
  921. const safeStr = (v) => (v == null ? '' : String(v));
  922. const isMsgId = (s) => typeof s === 'string' && (s.startsWith('%') || s.startsWith('&') || s.startsWith('@'));
  923. const fmtDate = (v) => {
  924. if (!v) return '';
  925. const m = moment(v, moment.ISO_8601, true);
  926. if (m.isValid()) return m.format('YYYY-MM-DD HH:mm:ss');
  927. const n = typeof v === 'number' ? v : Date.parse(v);
  928. if (!Number.isFinite(n)) return '';
  929. return moment(n).format('YYYY-MM-DD HH:mm:ss');
  930. };
  931. const renderField = (labelText, valueNode) => {
  932. if (valueNode == null || valueNode === '') return null;
  933. return div(
  934. { class: 'card-field' },
  935. span({ class: 'card-label' }, labelText),
  936. span({ class: 'card-value' }, valueNode)
  937. );
  938. };
  939. const entityTitle = (c) => {
  940. const t = String(c.type || '').toLowerCase();
  941. if (t === 'votes') return safeStr(c.question || c.title);
  942. if (t === 'bookmark') return safeStr(c.title || c.name || c.url);
  943. if (t === 'market') return safeStr(c.title);
  944. if (t === 'project') return safeStr(c.title);
  945. if (t === 'job') return safeStr(c.title);
  946. if (t === 'report') return safeStr(c.title);
  947. if (t === 'task') return safeStr(c.title);
  948. if (t === 'event') return safeStr(c.title);
  949. if (t === 'document') return safeStr(c.title || c.name || c.url);
  950. if (t === 'image' || t === 'audio' || t === 'video') return safeStr(c.title || c.name || c.url);
  951. return safeStr(c.title || c.name || c.question || c.url);
  952. };
  953. const renderEntityRoot = (c) => {
  954. const t = String(c.type || '').toLowerCase();
  955. const header = `[${safeUpper(t)}]`;
  956. const titleText = entityTitle(c) || '(sin título)';
  957. const nodes = [];
  958. nodes.push(
  959. div(
  960. { class: 'card-field', style: 'margin-bottom:10px;' },
  961. span({ class: 'card-label', style: 'font-weight:800;' }, header),
  962. span({ class: 'card-value', style: 'margin-left:10px; font-weight:800;' }, titleText)
  963. )
  964. );
  965. if (t === 'votes') {
  966. const status = safeStr(c.status);
  967. const deadline = fmtDate(c.deadline);
  968. const totalVotes = (typeof c.totalVotes !== 'undefined') ? safeStr(c.totalVotes) : '';
  969. const tags = Array.isArray(c.tags) ? c.tags.filter(Boolean) : [];
  970. const f1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  971. const f2 = renderField((i18n.deadline || 'Deadline') + ':', deadline);
  972. const f3 = renderField((i18n.voteTotalVotes || 'Total votes') + ':', totalVotes);
  973. if (f1) nodes.push(f1);
  974. if (f2) nodes.push(f2);
  975. if (f3) nodes.push(f3);
  976. if (tags.length) {
  977. nodes.push(
  978. div(
  979. { class: 'card-tags', style: 'margin-top:10px;' },
  980. ...tags.map(tag =>
  981. a(
  982. { href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' },
  983. `#${tag}`
  984. )
  985. )
  986. )
  987. );
  988. }
  989. } else if (t === 'report') {
  990. const status = safeStr(c.status);
  991. const severity = safeStr(c.severity);
  992. const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  993. const r2 = renderField((i18n.severity || 'Severity') + ':', severity ? safeUpper(severity) : '');
  994. if (r1) nodes.push(r1);
  995. if (r2) nodes.push(r2);
  996. } else if (t === 'task') {
  997. const status = safeStr(c.status);
  998. const priority = safeStr(c.priority);
  999. const startTime = fmtDate(c.startTime);
  1000. const endTime = fmtDate(c.endTime);
  1001. const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  1002. const r2 = renderField((i18n.priority || 'Priority') + ':', priority ? safeUpper(priority) : '');
  1003. const r3 = renderField((i18n.taskStartTimeLabel || 'Start') + ':', startTime);
  1004. const r4 = renderField((i18n.taskEndTimeLabel || 'End') + ':', endTime);
  1005. if (r1) nodes.push(r1);
  1006. if (r2) nodes.push(r2);
  1007. if (r3) nodes.push(r3);
  1008. if (r4) nodes.push(r4);
  1009. } else if (t === 'event') {
  1010. const dateStr = fmtDate(c.date);
  1011. const location = safeStr(c.location);
  1012. const price = (typeof c.price !== 'undefined') ? safeStr(c.price) : '';
  1013. const r1 = renderField((i18n.date || 'Date') + ':', dateStr);
  1014. const r2 = renderField((i18n.location || 'Location') + ':', location);
  1015. const r3 = renderField((i18n.price || 'Price') + ':', price ? `${price} ECO` : '');
  1016. if (r1) nodes.push(r1);
  1017. if (r2) nodes.push(r2);
  1018. if (r3) nodes.push(r3);
  1019. } else if (t === 'bookmark') {
  1020. const u = safeStr(c.url);
  1021. if (u) {
  1022. nodes.push(
  1023. renderField((i18n.url || 'URL') + ':', a({ href: u, target: '_blank', rel: 'noopener noreferrer' }, u))
  1024. );
  1025. }
  1026. } else if (t === 'image') {
  1027. const u = safeStr(c.url);
  1028. if (u && isMsgId(u)) {
  1029. nodes.push(
  1030. div({ class: 'card-field', style: 'margin-top:10px;' },
  1031. img({ src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' })
  1032. )
  1033. );
  1034. }
  1035. } else if (t === 'audio') {
  1036. const u = safeStr(c.url);
  1037. if (u && isMsgId(u)) {
  1038. nodes.push(
  1039. div({ class: 'card-field', style: 'margin-top:10px;' },
  1040. audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` })
  1041. )
  1042. );
  1043. }
  1044. } else if (t === 'video') {
  1045. const u = safeStr(c.url);
  1046. if (u && isMsgId(u)) {
  1047. nodes.push(
  1048. div({ class: 'card-field', style: 'margin-top:10px;' },
  1049. videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` })
  1050. )
  1051. );
  1052. }
  1053. } else if (t === 'document') {
  1054. const u = safeStr(c.url);
  1055. if (u && isMsgId(u)) {
  1056. const safeId = String(msg.key || u).replace(/[^a-zA-Z0-9_-]/g, '');
  1057. nodes.push(
  1058. div({ class: 'card-field', style: 'margin-top:10px;' },
  1059. div({
  1060. id: `pdf-container-${safeId}`,
  1061. class: 'pdf-viewer-container',
  1062. 'data-pdf-url': `/blob/${encodeURIComponent(u)}`
  1063. })
  1064. )
  1065. );
  1066. }
  1067. } else if (t === 'market') {
  1068. const status = safeStr(c.status);
  1069. const price = (typeof c.price !== 'undefined') ? safeStr(c.price) : '';
  1070. const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  1071. const r2 = renderField((i18n.price || 'Price') + ':', price ? `${price} ECO` : '');
  1072. if (r1) nodes.push(r1);
  1073. if (r2) nodes.push(r2);
  1074. } else if (t === 'project') {
  1075. const status = safeStr(c.status);
  1076. const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  1077. if (r1) nodes.push(r1);
  1078. } else if (t === 'job') {
  1079. const status = safeStr(c.status);
  1080. const location = safeStr(c.location);
  1081. const salary = (typeof c.salary !== 'undefined') ? safeStr(c.salary) : '';
  1082. const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
  1083. const r2 = renderField((i18n.jobLocation || 'Location') + ':', location ? safeUpper(location) : '');
  1084. const r3 = renderField((i18n.jobSalary || 'Salary') + ':', salary ? `${salary} ECO` : '');
  1085. if (r1) nodes.push(r1);
  1086. if (r2) nodes.push(r2);
  1087. if (r3) nodes.push(r3);
  1088. }
  1089. return article({ class: 'content' }, ...nodes.filter(Boolean));
  1090. };
  1091. const rawText = content.text || "";
  1092. const emptyContent = "<p>undefined</p>\n";
  1093. const isProbablyHtml =
  1094. typeof rawText === "string" &&
  1095. /<\/?[a-z][\s\S]*>/i.test(rawText.trim());
  1096. let articleElement;
  1097. if (contentType !== 'post' && contentType !== 'blog' && THREAD_ENTITY_TYPES.has(contentType)) {
  1098. articleElement = renderEntityRoot(content);
  1099. } else if (rawText === emptyContent) {
  1100. articleElement = article(
  1101. { class: "content" },
  1102. div(
  1103. { class: "card-field", style: "margin-bottom:10px;" },
  1104. span({ class: "card-label" }, (i18n.invalidPost || 'Invalid content') + ':'),
  1105. span({ class: "card-value" }, (i18n.invalidPostHint || 'This message has invalid/empty text.'))
  1106. ),
  1107. details(
  1108. summary(i18n.viewJson || 'View JSON'),
  1109. pre({
  1110. innerHTML: highlightJs.highlight(
  1111. JSON.stringify(msg, null, 2),
  1112. { language: "json", ignoreIllegals: true }
  1113. ).value,
  1114. })
  1115. )
  1116. );
  1117. } else if (isProbablyHtml) {
  1118. let html = rawText;
  1119. if (!/<a\b[^>]*>/i.test(html)) {
  1120. html = html.replace(
  1121. /(https?:\/\/[^\s<]+)/g,
  1122. (u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${u}</a>`
  1123. );
  1124. }
  1125. articleElement = article({ class: "content", innerHTML: html });
  1126. } else {
  1127. articleElement = article(
  1128. { class: "content" },
  1129. p({ class: "post-text" }, ...renderUrl(rawText))
  1130. );
  1131. }
  1132. if (preview) {
  1133. return section(
  1134. { id: msg.key, class: "post-preview" },
  1135. hasContentWarning
  1136. ? details(summary(msg.value?.content?.contentWarning), articleElement)
  1137. : articleElement
  1138. );
  1139. }
  1140. const ts_received = msg.value?.meta?.timestamp?.received;
  1141. const iso =
  1142. (ts_received && ts_received.iso8601) ||
  1143. (typeof msg.value?.timestamp === 'number' ? new Date(msg.value.timestamp).toISOString() : null) ||
  1144. (content.createdAt ? new Date(content.createdAt).toISOString() : null);
  1145. if (!iso || !moment(iso, moment.ISO_8601, true).isValid()) {
  1146. return null;
  1147. }
  1148. const validTimestamp = moment(iso, moment.ISO_8601);
  1149. const timeAgo = validTimestamp.fromNow();
  1150. const timeAbsolute = validTimestamp.toISOString().split(".")[0].replace("T", " ");
  1151. const likeButton = msg.value?.meta?.voted
  1152. ? { value: 0, class: "liked" }
  1153. : { value: 1, class: null };
  1154. const likeCount = msg.value?.meta?.votes?.length || 0;
  1155. const maxLikedNameLength = 16;
  1156. const maxLikedNames = 16;
  1157. const likedByNames = msg.value?.meta?.votes
  1158. .slice(0, maxLikedNames)
  1159. .map((person) => person.name)
  1160. .map((n) => n.slice(0, maxLikedNameLength))
  1161. .join(", ");
  1162. const additionalLikesMessage =
  1163. likeCount > maxLikedNames ? `+${likeCount - maxLikedNames} more` : ``;
  1164. const likedByMessage =
  1165. likeCount > 0 ? `${likedByNames} ${additionalLikesMessage}` : null;
  1166. const messageClasses = ["post"];
  1167. const recps = [];
  1168. const addRecps = (recpsInfo) => {
  1169. recpsInfo.forEach((recp) => {
  1170. recps.push(
  1171. a(
  1172. { href: `/author/${encodeURIComponent(recp.feedId)}` },
  1173. img({ class: "avatar", src: recp.avatarUrl, alt: "" })
  1174. )
  1175. );
  1176. });
  1177. };
  1178. if (isPrivate) {
  1179. messageClasses.push("private");
  1180. addRecps(msg.value?.meta?.recpsInfo || []);
  1181. }
  1182. if (isThreadTarget) {
  1183. messageClasses.push("thread-target");
  1184. }
  1185. if (isBlocked) {
  1186. messageClasses.push("blocked");
  1187. return section(
  1188. {
  1189. id: msg.key,
  1190. class: messageClasses.join(" "),
  1191. },
  1192. i18n.relationshipBlockingPost
  1193. );
  1194. }
  1195. const articleContent = article(
  1196. { class: "content" },
  1197. hasContentWarning ? div({ class: "post-subject" }, msg.value?.content?.contentWarning) : null,
  1198. articleElement
  1199. );
  1200. const fragment = section(
  1201. {
  1202. id: msg.key,
  1203. class: messageClasses.join(" "),
  1204. },
  1205. header(
  1206. div(
  1207. { class: "header-content" },
  1208. a(
  1209. { href: url.author },
  1210. img({ class: "avatar-profile", src: url.avatar, alt: "" })
  1211. ),
  1212. span(
  1213. { class: "created-at" },
  1214. `${i18n.createdBy} `,
  1215. a({ href: url.author }, "@", name),
  1216. ` | ${timeAbsolute} | ${i18n.sendTime} `,
  1217. a({ href: url.link }, timeAgo)
  1218. ),
  1219. isPrivate ? "🔒" : null,
  1220. isPrivate ? recps : null
  1221. )
  1222. ),
  1223. articleContent,
  1224. footer(
  1225. div(
  1226. form(
  1227. { action: url.likeForm, method: "post" },
  1228. button(
  1229. {
  1230. name: "voteValue",
  1231. type: "submit",
  1232. value: likeButton.value,
  1233. class: likeButton.class,
  1234. title: likedByMessage,
  1235. },
  1236. `☉ ${likeCount}`
  1237. )
  1238. ),
  1239. a({ href: url.comment }, i18n.comment),
  1240. isPrivate || isRoot || isFork
  1241. ? null
  1242. : a({ href: url.subtopic }, nbsp, i18n.subtopic)
  1243. ),
  1244. br()
  1245. )
  1246. );
  1247. const threadSeparator = [br()];
  1248. if (aside) {
  1249. return [fragment, postAside(msg), isRoot ? threadSeparator : null];
  1250. } else {
  1251. return fragment;
  1252. }
  1253. };
  1254. exports.editProfileView = ({ name, description }) =>
  1255. template(
  1256. i18n.editProfile,
  1257. section(
  1258. h1(i18n.editProfile),
  1259. p(i18n.editProfileDescription),
  1260. form(
  1261. {
  1262. action: "/profile/edit",
  1263. method: "POST",
  1264. enctype: "multipart/form-data",
  1265. },
  1266. label(
  1267. i18n.profileImage,
  1268. br,
  1269. input({ type: "file", name: "image", accept: "image/*" })
  1270. ),
  1271. br,br,
  1272. label(i18n.profileName,
  1273. br,
  1274. input({ name: "name", value: name })),
  1275. br,br,
  1276. label(
  1277. i18n.profileDescription,
  1278. br,
  1279. textarea(
  1280. {
  1281. autofocus: true,
  1282. name: "description",
  1283. rows: "6",
  1284. },
  1285. description
  1286. )
  1287. ),
  1288. br,
  1289. button(
  1290. {
  1291. type: "submit",
  1292. },
  1293. i18n.submit
  1294. )
  1295. )
  1296. )
  1297. );
  1298. exports.authorView = ({
  1299. avatarUrl,
  1300. description,
  1301. feedId,
  1302. messages,
  1303. firstPost,
  1304. lastPost,
  1305. name,
  1306. relationship,
  1307. ecoAddress,
  1308. karmaScore = 0,
  1309. lastActivityBucket
  1310. }) => {
  1311. const linkUrl = `/author/${encodeURIComponent(feedId)}`;
  1312. const mention = `[@${name}](${feedId})`;
  1313. const markdownMention = highlightJs.highlight(mention, { language: "markdown", ignoreIllegals: true }).value;
  1314. const contactForms = [];
  1315. const addForm = ({ action }) =>
  1316. contactForms.push(
  1317. form(
  1318. { action: `/${action}/${encodeURIComponent(feedId)}`, method: "post" },
  1319. button({ type: "submit" }, i18n[action])
  1320. )
  1321. );
  1322. if (relationship.me === false) {
  1323. if (relationship.following) addForm({ action: "unfollow" });
  1324. else if (relationship.blocking) addForm({ action: "unblock" });
  1325. else { addForm({ action: "follow" }); addForm({ action: "block" }) }
  1326. }
  1327. const relationshipMessage = (() => {
  1328. if (relationship.me) return i18n.relationshipYou;
  1329. const following = relationship.following === true;
  1330. const followsMe = relationship.followsMe === true;
  1331. if (following && followsMe) return i18n.relationshipMutuals;
  1332. const messagesArr = [];
  1333. messagesArr.push(following ? i18n.relationshipFollowing : i18n.relationshipNone);
  1334. messagesArr.push(followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing);
  1335. return messagesArr.join(". ") + ".";
  1336. })();
  1337. const bucket = lastActivityBucket || 'red';
  1338. const dotClass = bucket === "green" ? "green" : bucket === "orange" ? "orange" : "red";
  1339. const prefix = section(
  1340. { class: "message" },
  1341. div(
  1342. { class: "profile" },
  1343. div({ class: "avatar-container" },
  1344. img({ class: "inhabitant-photo-details", src: avatarUrl }),
  1345. h1({ class: "name" }, name),
  1346. ),
  1347. pre({ class: "md-mention", innerHTML: markdownMention }),
  1348. p(a({ class: "user-link", href: `/author/${encodeURIComponent(feedId)}` }, feedId)),
  1349. div({ class: "profile-metrics" },
  1350. p(`${i18n.bankingUserEngagementScore}: `, strong(karmaScore !== undefined ? karmaScore : 0)),
  1351. div({ class: "inhabitant-last-activity" },
  1352. span({ class: "label" }, `${i18n.inhabitantActivityLevel}:`),
  1353. span({ class: `activity-dot ${dotClass}` }, "")
  1354. ),
  1355. ecoAddress
  1356. ? div({ class: "eco-wallet" }, p(`${i18n.bankWalletConnected}: `, strong(ecoAddress)))
  1357. : div({ class: "eco-wallet" }, p(i18n.ecoWalletNotConfigured || "ECOin Wallet not configured"))
  1358. )
  1359. ),
  1360. description !== "" ? article({ innerHTML: markdown(description) }) : null,
  1361. footer(
  1362. div(
  1363. { class: "profile" },
  1364. ...contactForms.map(form => span({ style: "font-weight: bold;" }, form)),
  1365. relationship.me
  1366. ? span({ class: "status you" }, i18n.relationshipYou)
  1367. : div({ class: "relationship-status" },
  1368. relationship.blocking && relationship.blockedBy
  1369. ? span({ class: "status blocked" }, i18n.relationshipMutualBlock)
  1370. : [
  1371. relationship.blocking ? span({ class: "status blocked" }, i18n.relationshipBlocking) : null,
  1372. relationship.blockedBy ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy) : null,
  1373. relationship.following && relationship.followsMe
  1374. ? span({ class: "status mutual" }, i18n.relationshipMutuals)
  1375. : [
  1376. span({ class: "status supporting" }, relationship.following ? i18n.relationshipFollowing : i18n.relationshipNone),
  1377. span({ class: "status supported-by" }, relationship.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing)
  1378. ]
  1379. ]
  1380. ),
  1381. relationship.me ? a({ href: `/profile/edit`, class: "btn" }, nbsp, i18n.editProfile) : null,
  1382. a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes),
  1383. !relationship.me ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "btn" }, i18n.pmCreateButton) : null
  1384. )
  1385. )
  1386. );
  1387. let items = messages.map((msg) => post({ msg }));
  1388. if (items.length === 0) {
  1389. if (lastPost === undefined) {
  1390. items.push(section(div(span(i18n.feedEmpty))));
  1391. } else {
  1392. items.push(
  1393. section(
  1394. div(
  1395. span(i18n.feedRangeEmpty),
  1396. a({ href: `${linkUrl}` }, i18n.seeFullFeed)
  1397. )
  1398. )
  1399. );
  1400. }
  1401. } else {
  1402. const highestSeqNum = messages[0].value.sequence;
  1403. const lowestSeqNum = messages[messages.length - 1].value.sequence;
  1404. const newerPostsLink = a(
  1405. {
  1406. href:
  1407. lastPost !== undefined && highestSeqNum < lastPost.value.sequence
  1408. ? `${linkUrl}?gt=${highestSeqNum}`
  1409. : "#",
  1410. class:
  1411. lastPost !== undefined && highestSeqNum < lastPost.value.sequence
  1412. ? "btn"
  1413. : "btn disabled",
  1414. "aria-disabled":
  1415. lastPost === undefined || highestSeqNum >= lastPost.value.sequence
  1416. },
  1417. i18n.newerPosts
  1418. );
  1419. const olderPostsLink = a(
  1420. {
  1421. href:
  1422. lowestSeqNum > firstPost.value.sequence
  1423. ? `${linkUrl}?lt=${lowestSeqNum}`
  1424. : "#",
  1425. class:
  1426. lowestSeqNum > firstPost.value.sequence
  1427. ? "btn"
  1428. : "btn disabled",
  1429. "aria-disabled": !(lowestSeqNum > firstPost.value.sequence)
  1430. },
  1431. i18n.olderPosts
  1432. );
  1433. const pagination = section(
  1434. { class: "message" },
  1435. footer(div(newerPostsLink, olderPostsLink), br())
  1436. );
  1437. items.unshift(pagination);
  1438. items.push(pagination);
  1439. }
  1440. return template(i18n.profile, prefix, items);
  1441. };
  1442. exports.previewCommentView = async ({
  1443. previewData,
  1444. messages,
  1445. myFeedId,
  1446. parentMessage,
  1447. contentWarning,
  1448. }) => {
  1449. if (!parentMessage || !parentMessage.value) {
  1450. throw new Error("Missing parentMessage or value");
  1451. }
  1452. const publishAction = `/comment/${encodeURIComponent(parentMessage.key)}`;
  1453. const preview = generatePreview({
  1454. previewData,
  1455. contentWarning,
  1456. action: publishAction,
  1457. });
  1458. return exports.commentView(
  1459. { messages, myFeedId, parentMessage },
  1460. preview,
  1461. previewData.text,
  1462. contentWarning
  1463. );
  1464. };
  1465. exports.commentView = async (
  1466. { messages, myFeedId, parentMessage },
  1467. preview,
  1468. text,
  1469. contentWarning
  1470. ) => {
  1471. if (!parentMessage || !parentMessage.value) {
  1472. throw new Error("Missing parentMessage or value");
  1473. }
  1474. const parentKey = parentMessage.key;
  1475. const threadRoot = parentMessage.value?.content?.root || parentKey;
  1476. const messagesInput = Array.isArray(messages) ? messages : [];
  1477. const merged = [parentMessage, ...messagesInput];
  1478. const filtered = merged.filter((m) => {
  1479. if (!m || !m.value) return false;
  1480. return m.key === threadRoot || m.value?.content?.root === threadRoot;
  1481. });
  1482. const seen = new Set();
  1483. const threadMessages = [];
  1484. for (const m of filtered) {
  1485. if (m && m.key && !seen.has(m.key)) {
  1486. seen.add(m.key);
  1487. threadMessages.push(m);
  1488. }
  1489. }
  1490. const tsNum = (m) => {
  1491. const n1 = Number(m?.value?.timestamp);
  1492. if (Number.isFinite(n1) && n1 > 0) return n1;
  1493. const iso = m?.value?.meta?.timestamp?.received?.iso8601;
  1494. const raw = m?.value?.meta?.timestamp?.received;
  1495. const n2 = iso ? Date.parse(iso) : (raw ? Date.parse(raw) : NaN);
  1496. if (Number.isFinite(n2) && n2 > 0) return n2;
  1497. const createdAt = m?.value?.content?.createdAt;
  1498. const n3 = createdAt ? Date.parse(createdAt) : NaN;
  1499. if (Number.isFinite(n3) && n3 > 0) return n3;
  1500. return 0;
  1501. };
  1502. threadMessages.sort((a, b) => tsNum(a) - tsNum(b));
  1503. const authorName = parentMessage.value?.meta?.author?.name || parentMessage.value?.author || "Anonymous";
  1504. let markdownMention = "";
  1505. const parentAuthorFeedId = parentMessage.value?.author;
  1506. const parentAuthorName =
  1507. parentMessage.value?.meta?.author?.name ||
  1508. (typeof parentAuthorFeedId === "string"
  1509. ? (parentAuthorFeedId.startsWith("@") ? parentAuthorFeedId.slice(1) : parentAuthorFeedId)
  1510. : "Anonymous");
  1511. if (parentAuthorFeedId && parentAuthorFeedId !== myFeedId) {
  1512. markdownMention = `[@${parentAuthorName}](${parentAuthorFeedId})\n\n`;
  1513. }
  1514. const messageElements = threadMessages.map((m) => post({ msg: m }));
  1515. const action = `/comment/preview/${encodeURIComponent(parentKey)}`;
  1516. const method = "post";
  1517. const isPrivate = Boolean(parentMessage.value?.meta?.private);
  1518. return template(
  1519. i18n.commentTitle({ authorName }),
  1520. div({ class: "thread-container" }, ...messageElements),
  1521. form(
  1522. { action, method, enctype: "multipart/form-data" },
  1523. i18n.blogSubject,
  1524. br,
  1525. label(
  1526. i18n.contentWarningLabel,
  1527. input({
  1528. name: "contentWarning",
  1529. type: "text",
  1530. class: "contentWarning",
  1531. value: contentWarning ? contentWarning : "",
  1532. placeholder: i18n.contentWarningPlaceholder
  1533. })
  1534. ),
  1535. br,
  1536. label({ for: "text" }, i18n.blogMessage),
  1537. br,
  1538. textarea(
  1539. {
  1540. autofocus: true,
  1541. required: true,
  1542. name: "text",
  1543. rows: "6",
  1544. cols: "50",
  1545. placeholder: i18n.publishWarningPlaceholder
  1546. },
  1547. text ? text : null
  1548. ),
  1549. br,
  1550. label(
  1551. { for: "blob" },
  1552. i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"
  1553. ),
  1554. input({ type: "file", id: "blob", name: "blob" }),
  1555. br,
  1556. br,
  1557. button({ type: "submit" }, i18n.blogPublish)
  1558. ),
  1559. preview ? div({ class: "comment-preview" }, preview) : ""
  1560. );
  1561. };
  1562. const renderMessage = (msg) => {
  1563. const content = lodash.get(msg, "value.content", {});
  1564. const author = msg.value.author || "Anonymous";
  1565. const createdAt = new Date(msg.value.timestamp).toLocaleString();
  1566. const mentionsText = content.text || '';
  1567. return div({ class: "mention-item" }, [
  1568. div({ class: "mention-content", innerHTML: mentionsText || '[No content]' }),
  1569. p(a({ class: 'user-link', href: `/author/${encodeURIComponent(author)}` }, author)),
  1570. p(`${i18n.createdAtLabel || i18n.mentionsCreatedAt}: ${createdAt}`)
  1571. ]);
  1572. };
  1573. exports.mentionsView = ({ messages, myFeedId }) => {
  1574. const title = i18n.mentions;
  1575. const description = i18n.mentionsDescription;
  1576. if (!Array.isArray(messages) || messages.length === 0) {
  1577. return template(
  1578. title,
  1579. section(
  1580. div({ class: "tags-header" },
  1581. h2(title),
  1582. p(description)
  1583. )
  1584. ),
  1585. section(
  1586. div({ class: "mentions-list" },
  1587. p({ class: "empty" }, i18n.noMentions)
  1588. )
  1589. )
  1590. );
  1591. }
  1592. const filteredMessages = messages.filter(msg => {
  1593. const mentions = lodash.get(msg, "value.content.mentions", {});
  1594. return Object.keys(mentions).some(key => mentions[key].link === myFeedId);
  1595. });
  1596. if (filteredMessages.length === 0) {
  1597. return template(
  1598. title,
  1599. section(
  1600. div({ class: "tags-header" },
  1601. h2(title),
  1602. p(description)
  1603. )
  1604. ),
  1605. section(
  1606. div({ class: "mentions-list" },
  1607. p({ class: "empty" }, i18n.noMentions)
  1608. )
  1609. )
  1610. );
  1611. }
  1612. return template(
  1613. title,
  1614. section(
  1615. div({ class: "tags-header" },
  1616. h2(title),
  1617. p(description)
  1618. )
  1619. ),
  1620. section(
  1621. div({ class: "mentions-list" },
  1622. filteredMessages.map(renderMessage)
  1623. )
  1624. )
  1625. );
  1626. };
  1627. exports.privateView = async (messagesInput, filter) => {
  1628. const messagesRaw = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages
  1629. const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true)
  1630. const userId = await getUserId()
  1631. const isSent = m => (m?.value?.author === userId) || (m?.value?.content?.from === userId)
  1632. const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId)
  1633. const linkAuthor = (id) =>
  1634. a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id)
  1635. const hrefFor = {
  1636. job: (id) => `/jobs/${encodeURIComponent(id)}`,
  1637. project: (id) => `/projects/${encodeURIComponent(id)}`,
  1638. market: (id) => `/market/${encodeURIComponent(id)}`
  1639. }
  1640. const clickableCardProps = (href, extraClass = '') => {
  1641. const props = { class: `pm-card ${extraClass}` }
  1642. if (href) {
  1643. props.onclick = `window.location='${href}'`
  1644. props.tabindex = 0
  1645. props.onkeypress = `if(event.key==='Enter') window.location='${href}'`
  1646. }
  1647. return props
  1648. }
  1649. const chip = (txt) => span({ class: 'chip' }, txt)
  1650. function headerLine({ sentAt, from, toLinks, textLen }) {
  1651. return div({ class: 'pm-header' },
  1652. span({ class: 'date-link' }, `${moment(sentAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`),
  1653. span({ class: 'pm-from' }, ' ', i18n.pmFromLabel, ' ', linkAuthor(from)),
  1654. span({ class: 'pm-to' }, ' ', '→', ' ', i18n.pmToLabel, ' ', toLinks)
  1655. )
  1656. }
  1657. function actions({ key, replyId, subjectRaw, text }) {
  1658. const stop = { onclick: 'event.stopPropagation()' }
  1659. const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}`
  1660. return div({ class: 'pm-actions' },
  1661. form({ method: 'GET', action: '/pm', class: 'pm-action-form', ...stop },
  1662. input({ type: 'hidden', name: 'recipients', value: replyId }),
  1663. input({ type: 'hidden', name: 'subject', value: subjectReply }),
  1664. input({ type: 'hidden', name: 'quote', value: text || '' }),
  1665. button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase())
  1666. ),
  1667. form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop },
  1668. button({ type: 'submit', class: 'pm-btn delete-btn' }, i18n.privateDelete.toUpperCase())
  1669. )
  1670. )
  1671. }
  1672. function canonicalSubject(s) {
  1673. return (s || '').replace(/^\s*(RE:\s*)+/i, '').trim()
  1674. }
  1675. function participantsKey(m) {
  1676. const c = m?.value?.content || {}
  1677. const set = new Set([m?.value?.author, ...(Array.isArray(c.to) ? c.to : [])])
  1678. return Array.from(set).sort().join('|')
  1679. }
  1680. function threadId(m) {
  1681. return canonicalSubject(m?.value?.content?.subject || '') + '||' + participantsKey(m)
  1682. }
  1683. function threadLevel(s) {
  1684. const m = (s || '').match(/RE:/gi)
  1685. return m ? Math.min(m.length, 8) : 0
  1686. }
  1687. function quoted(str) {
  1688. const m = str.match(/"([^"]+)"/)
  1689. return m ? m[1] : ''
  1690. }
  1691. function pickLink(str, kind) {
  1692. if (kind === 'job') {
  1693. const m = str.match(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/)
  1694. return m ? m[1] : ''
  1695. }
  1696. if (kind === 'project') {
  1697. const m = str.match(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/)
  1698. return m ? m[1] : ''
  1699. }
  1700. if (kind === 'market') {
  1701. const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/)
  1702. return m ? m[1] : ''
  1703. }
  1704. return ''
  1705. }
  1706. function clickableLinks(str) {
  1707. return str
  1708. .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `<a class="user-link" href="/author/${encodeURIComponent(id)}">${match}</a>`)
  1709. .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="job-link" href="${hrefFor.job(id)}">${match}</a>`)
  1710. .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="project-link" href="${hrefFor.project(id)}">${match}</a>`)
  1711. .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="market-link" href="${hrefFor.market(id)}">${match}</a>`)
  1712. }
  1713. const threads = {}
  1714. for (const m of messages) {
  1715. const tid = threadId(m)
  1716. if (!threads[tid]) threads[tid] = []
  1717. threads[tid].push(m)
  1718. }
  1719. const inboxSet = new Set()
  1720. for (const arr of Object.values(threads)) {
  1721. const hasInbound = arr.some(isToUser)
  1722. if (hasInbound) for (const m of arr) inboxSet.add(m)
  1723. }
  1724. const data =
  1725. filter === 'sent' ? messages.filter(isSent) :
  1726. filter === 'inbox' ? Array.from(inboxSet) :
  1727. messages
  1728. const inboxCount = Array.from(inboxSet).length
  1729. const sentCount = messages.filter(isSent).length
  1730. const sorted = [...data].sort((a, b) => {
  1731. const ta = threadId(a)
  1732. const tb = threadId(b)
  1733. if (ta < tb) return -1
  1734. if (ta > tb) return 1
  1735. const sa = new Date(a?.value?.content?.sentAt || a.timestamp || 0).getTime()
  1736. const sb = new Date(b?.value?.content?.sentAt || b.timestamp || 0).getTime()
  1737. return sa - sb
  1738. })
  1739. function JobCard({ type, sentAt, from, toLinks, text, key }) {
  1740. const isSub = type === 'JOB_SUBSCRIBED'
  1741. const icon = isSub ? '🟡' : '🟠'
  1742. const titleH = isSub ? (i18n.inboxJobSubscribedTitle || 'New subscription to your job offer') : (i18n.inboxJobUnsubscribedTitle || 'Unsubscription from your job offer')
  1743. const jobTitle = quoted(text) || 'job'
  1744. const jobId = pickLink(text, 'job')
  1745. const href = jobId ? hrefFor.job(jobId) : null
  1746. return div(
  1747. clickableCardProps(href, `job-notification thread-level-0`),
  1748. headerLine({ sentAt, from, toLinks, textLen: text.length }),
  1749. h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`),
  1750. p(
  1751. i18n.pmInhabitantWithId, ' ',
  1752. linkAuthor(from), ' ',
  1753. isSub ? i18n.pmHasSubscribedToYourJobOffer : (i18n.pmHasUnsubscribedFromYourJobOffer || 'has unsubscribed from your job offer'),
  1754. ' ',
  1755. href ? a({ class: 'job-link', href }, `"${jobTitle}"`) : `"${jobTitle}"`
  1756. ),
  1757. actions({ key, replyId: from, subjectRaw: jobTitle, text })
  1758. )
  1759. }
  1760. function ProjectFollowCard({ type, sentAt, from, toLinks, text, key }) {
  1761. const isFollow = type === 'PROJECT_FOLLOWED'
  1762. const icon = isFollow ? '🔔' : '🔕'
  1763. const titleH = isFollow
  1764. ? (i18n.inboxProjectFollowedTitle || 'New follower of your project')
  1765. : (i18n.inboxProjectUnfollowedTitle || 'Unfollowed your project')
  1766. const projectTitle = quoted(text) || 'project'
  1767. const projectId = pickLink(text, 'project')
  1768. const href = projectId ? hrefFor.project(projectId) : null
  1769. return div(
  1770. clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`),
  1771. headerLine({ sentAt, from, toLinks, textLen: text.length }),
  1772. h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`),
  1773. p(
  1774. i18n.pmInhabitantWithId, ' ',
  1775. a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}` }, from),
  1776. ' ',
  1777. isFollow ? (i18n.pmHasFollowedYourProject || 'has followed your project') : (i18n.pmHasUnfollowedYourProject || 'has unfollowed your project'),
  1778. ' ',
  1779. href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
  1780. ),
  1781. actions({ key, replyId: from, subjectRaw: projectTitle, text })
  1782. )
  1783. }
  1784. function MarketSoldCard({ sentAt, from, toLinks, subject, text, key }) {
  1785. const itemTitle = quoted(subject) || quoted(text) || 'item'
  1786. const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from
  1787. const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || ''
  1788. const marketId = pickLink(text, 'market')
  1789. const href = marketId ? hrefFor.market(marketId) : null
  1790. return div(
  1791. clickableCardProps(href, 'market-sold-notification thread-level-0'),
  1792. headerLine({ sentAt, from, toLinks, textLen: text.length }),
  1793. h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`),
  1794. p(
  1795. i18n.pmYourItem, ' ',
  1796. href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
  1797. ' ',
  1798. i18n.pmHasBeenSoldTo, ' ',
  1799. linkAuthor(buyerId),
  1800. price ? ` ${i18n.pmFor} $${price}.` : '.'
  1801. ),
  1802. actions({ key, replyId: buyerId, subjectRaw: itemTitle, text })
  1803. )
  1804. }
  1805. function ProjectPledgeCard({ sentAt, from, toLinks, content, text, key }) {
  1806. const amount = content.meta?.amount ?? (text.match(/pledged\s+([\d.]+)/)?.[1] || '0')
  1807. const projectTitle = content.meta?.projectTitle ?? (text.match(/project\s+"([^"]+)"/)?.[1] || 'project')
  1808. const projectId = content.meta?.projectId ?? pickLink(text, 'project')
  1809. const href = projectId ? hrefFor.project(projectId) : null
  1810. return div(
  1811. clickableCardProps(href, 'project-pledge-notification thread-level-0'),
  1812. headerLine({ sentAt, from, toLinks, textLen: text.length }),
  1813. h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`),
  1814. p(
  1815. i18n.pmInhabitantWithId, ' ',
  1816. linkAuthor(from), ' ',
  1817. i18n.pmHasPledged, ' ',
  1818. chip(`${amount} ECO`), ' ',
  1819. i18n.pmToYourProject, ' ',
  1820. href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
  1821. ),
  1822. actions({ key, replyId: from, subjectRaw: projectTitle, text })
  1823. )
  1824. }
  1825. return template(
  1826. i18n.private,
  1827. section(
  1828. div({ class: 'tags-header' },
  1829. h2(i18n.private),
  1830. p(i18n.privateDescription)
  1831. ),
  1832. div({ class: 'filters' },
  1833. form({ method: 'GET', action: '/inbox' }, [
  1834. button({
  1835. type: 'submit',
  1836. name: 'filter',
  1837. value: 'inbox',
  1838. class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn'
  1839. }, `${i18n.privateInbox} (${inboxCount})`),
  1840. button({
  1841. type: 'submit',
  1842. name: 'filter',
  1843. value: 'sent',
  1844. class: filter === 'sent' ? 'filter-btn active' : 'filter-btn'
  1845. }, `${i18n.privateSent} (${sentCount})`),
  1846. button({
  1847. type: 'submit',
  1848. name: 'filter',
  1849. value: 'create',
  1850. class: 'create-button',
  1851. formaction: '/pm',
  1852. formmethod: 'GET'
  1853. }, i18n.pmCreateButton)
  1854. ])
  1855. ),
  1856. div({ class: 'message-list' },
  1857. sorted.length
  1858. ? sorted.map(msg => {
  1859. const content = msg.value.content
  1860. const author = msg.value.author
  1861. const subjectRaw = content.subject || ''
  1862. const subjectU = subjectRaw.toUpperCase()
  1863. const text = content.text || ''
  1864. const sentAt = new Date(content.sentAt || msg.timestamp)
  1865. const fromResolved = content.from || author
  1866. const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : []
  1867. const level = threadLevel(subjectRaw)
  1868. if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') {
  1869. return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
  1870. }
  1871. if (subjectU === 'PROJECT_FOLLOWED' || subjectU === 'PROJECT_UNFOLLOWED') {
  1872. return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
  1873. }
  1874. if (subjectU === 'MARKET_SOLD') {
  1875. return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key })
  1876. }
  1877. if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') {
  1878. return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key })
  1879. }
  1880. return div(
  1881. { class: `pm-card normal-pm thread-level-${level}` },
  1882. headerLine({ sentAt, from: fromResolved, toLinks, textLen: text.length }),
  1883. h2(subjectRaw || i18n.pmNoSubject),
  1884. p({ class: 'message-text' }, ...renderUrl(clickableLinks(text))),
  1885. actions({ key: msg.key, replyId: fromResolved, subjectRaw, text })
  1886. )
  1887. })
  1888. : p({ class: 'empty' }, i18n.noPrivateMessages)
  1889. )
  1890. )
  1891. )
  1892. }
  1893. exports.publishCustomView = async () => {
  1894. const action = "/publish/custom";
  1895. const method = "post";
  1896. return template(
  1897. i18n.publishCustom,
  1898. section(
  1899. div({ class: "tags-header" },
  1900. h2(i18n.publishCustom),
  1901. p(i18n.publishCustomDescription)
  1902. ),
  1903. form(
  1904. { action, method },
  1905. textarea(
  1906. {
  1907. autofocus: true,
  1908. required: true,
  1909. name: "text",
  1910. rows: 10,
  1911. style: "width: 100%;"
  1912. },
  1913. "{\n",
  1914. ' "type": "feed",\n',
  1915. ' "hello": "world"\n',
  1916. "}"
  1917. ),
  1918. br,
  1919. br,
  1920. button({ type: "submit" }, i18n.submit)
  1921. )
  1922. ),
  1923. section(
  1924. div({ class: "tags-header" },
  1925. p(i18n.publishBasicInfo({ href: "/publish" }))
  1926. )
  1927. )
  1928. );
  1929. };
  1930. exports.threadView = ({ messages }) => {
  1931. const rootMessage = messages[0];
  1932. const rootAuthorName = rootMessage.value.meta.author.name;
  1933. const needsPdfViewer = Array.isArray(messages) && messages.some((m) => {
  1934. const t = String(m?.value?.content?.type || "").toLowerCase();
  1935. return t === "document";
  1936. });
  1937. const tpl = template(
  1938. [`@${rootAuthorName}`],
  1939. div(thread(messages))
  1940. );
  1941. return `${tpl}${
  1942. needsPdfViewer
  1943. ? `<script type="module" src="/js/pdf.min.mjs"></script>
  1944. <script src="/js/pdf-viewer.js"></script>`
  1945. : ""
  1946. }`;
  1947. };
  1948. exports.publishView = (preview, text, contentWarning) => {
  1949. return template(
  1950. i18n.publish,
  1951. section(
  1952. div({ class: "tags-header" },
  1953. h2(i18n.publishBlog),
  1954. p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" }))
  1955. )
  1956. ),
  1957. section(
  1958. div({ class: "publish-form" },
  1959. form(
  1960. {
  1961. action: "/publish/preview",
  1962. method: "post",
  1963. enctype: "multipart/form-data",
  1964. },
  1965. [
  1966. label({ for: "contentWarning" }, i18n.blogSubject),
  1967. br(),
  1968. input({
  1969. name: "contentWarning",
  1970. id: "contentWarning",
  1971. type: "text",
  1972. class: "contentWarning",
  1973. value: contentWarning || "",
  1974. placeholder: i18n.contentWarningPlaceholder
  1975. }),
  1976. br(),
  1977. label({ for: "text" }, i18n.blogMessage),
  1978. br(),
  1979. textarea(
  1980. {
  1981. required: true,
  1982. name: "text",
  1983. id: "text",
  1984. rows: "6",
  1985. cols: "50",
  1986. placeholder: i18n.publishWarningPlaceholder,
  1987. class: "publish-textarea"
  1988. },
  1989. text || ""
  1990. ),
  1991. br(),
  1992. label({ for: "blob" }, i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"),
  1993. br(),
  1994. input({ type: "file", id: "blob", name: "blob" }),
  1995. br(), br(),
  1996. button({ type: "submit" }, i18n.blogPublish)
  1997. ]
  1998. )
  1999. )
  2000. ),
  2001. preview || "",
  2002. section(
  2003. div({ class: "tags-header" },
  2004. p(i18n.publishCustomInfo({ href: "/publish/custom" }))
  2005. )
  2006. )
  2007. );
  2008. };
  2009. const generatePreview = ({ previewData, contentWarning, action }) => {
  2010. const { authorMeta, formattedText, mentions } = previewData;
  2011. const renderedText = formattedText;
  2012. const msg = {
  2013. key: "%non-existent.preview",
  2014. value: {
  2015. author: authorMeta.id,
  2016. content: {
  2017. type: "post",
  2018. text: renderedText,
  2019. mentions: mentions,
  2020. },
  2021. timestamp: Date.now(),
  2022. meta: {
  2023. isPrivate: false,
  2024. votes: [],
  2025. author: {
  2026. name: authorMeta.name,
  2027. avatar: {
  2028. url: `http://localhost:3000/blob/${encodeURIComponent(authorMeta.image)}`,
  2029. },
  2030. },
  2031. },
  2032. },
  2033. };
  2034. if (contentWarning) {
  2035. msg.value.content.contentWarning = contentWarning;
  2036. }
  2037. if (msg.value.meta.author.avatar.url === 'http://localhost:3000/blob/%260000000000000000000000000000000000000000000%3D.sha256') {
  2038. msg.value.meta.author.avatar.url = '/assets/images/default-avatar.png';
  2039. }
  2040. const ts = new Date(msg.value.timestamp);
  2041. lodash.set(msg, "value.meta.timestamp.received.iso8601", ts.toISOString());
  2042. const ago = Date.now() - Number(ts);
  2043. const prettyAgo = prettyMs(ago, { compact: true });
  2044. lodash.set(msg, "value.meta.timestamp.received.since", prettyAgo);
  2045. return div(
  2046. section(
  2047. { class: "post-preview" },
  2048. div(
  2049. { class: "preview-content" },
  2050. h2(i18n.messagePreview),
  2051. post({ msg, preview: true })
  2052. ),
  2053. ),
  2054. section(
  2055. { class: "mention-suggestions" },
  2056. Object.keys(mentions).map((name) => {
  2057. const matches = mentions[name];
  2058. return div(
  2059. h2(i18n.mentionsMatching),
  2060. { class: "mention-card" },
  2061. a(
  2062. {
  2063. href: `/author/@${encodeURIComponent(matches[0].feed)}`,
  2064. },
  2065. img({ src: msg.value.meta.author.avatar.url, class: "avatar-profile" })
  2066. ),
  2067. br,
  2068. div(
  2069. { class: "mention-name" },
  2070. span({ class: "label" }, `${i18n.mentionsName}: `),
  2071. a(
  2072. {
  2073. href: `/author/@${encodeURIComponent(matches[0].feed)}`,
  2074. },
  2075. `@${authorMeta.name}`
  2076. )
  2077. ),
  2078. div(
  2079. { class: "mention-relationship" },
  2080. span({ class: "label" }, `${i18n.mentionsRelationship}:`),
  2081. span({ class: "relationship" }, matches[0].rel.followsMe ? i18n.relationshipMutuals : i18n.relationshipNotMutuals),
  2082. { class: "mention-relationship-details" },
  2083. span({ class: "emoji" }, matches[0].rel.followsMe ? "☍" : "⚼"),
  2084. span({ class: "mentions-listing" },
  2085. a({ class: 'user-link', href: `/author/@${encodeURIComponent(matches[0].feed)}` }, `@${matches[0].feed}`)
  2086. )
  2087. )
  2088. );
  2089. })
  2090. ),
  2091. section(
  2092. form(
  2093. { action, method: "post" },
  2094. [
  2095. input({ type: "hidden", name: "text", value: renderedText }),
  2096. input({ type: "hidden", name: "contentWarning", value: contentWarning || "" }),
  2097. input({ type: "hidden", name: "mentions", value: JSON.stringify(mentions) }),
  2098. button({ type: "submit" }, i18n.publish)
  2099. ]
  2100. )
  2101. )
  2102. );
  2103. };
  2104. exports.previewView = ({ previewData, contentWarning }) => {
  2105. const publishAction = "/publish";
  2106. const preview = generatePreview({
  2107. previewData,
  2108. contentWarning,
  2109. action: publishAction,
  2110. });
  2111. return exports.publishView(preview, previewData.text || "", contentWarning);
  2112. };
  2113. const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => {
  2114. if (!viewTitle && !viewDescription) {
  2115. return null;
  2116. }
  2117. return section(
  2118. { class: "viewInfo" },
  2119. viewTitle ? h1(viewTitle) : null,
  2120. viewDescription ? em(viewDescription) : null
  2121. );
  2122. };
  2123. exports.likesView = async ({ messages, feed, name }) => {
  2124. const authorLink = a(
  2125. { href: `/author/${encodeURIComponent(feed)}` },
  2126. "@" + name
  2127. );
  2128. return template(
  2129. ["@", name],
  2130. viewInfoBox({
  2131. viewTitle: span(authorLink),
  2132. viewDescription: span(i18n.spreadedDescription)
  2133. }),
  2134. messages.map((msg) => post({ msg }))
  2135. );
  2136. };
  2137. const messageListView = ({
  2138. messages,
  2139. viewTitle = null,
  2140. viewDescription = null,
  2141. viewElements = null,
  2142. aside = null,
  2143. }) => {
  2144. const hasHeader = !!viewElements;
  2145. const titleBlock = hasHeader
  2146. ? viewElements
  2147. : div({ class: "tags-header" },
  2148. h2(viewTitle),
  2149. p(viewDescription)
  2150. );
  2151. return template(
  2152. viewTitle,
  2153. section(titleBlock),
  2154. messages.map((msg) => post({ msg, aside }))
  2155. );
  2156. };
  2157. exports.popularView = ({ messages, prefix }) => {
  2158. const header = div({ class: "tags-header" },
  2159. h2(i18n.popular),
  2160. p(i18n.popularDescription)
  2161. );
  2162. return messageListView({
  2163. messages,
  2164. viewTitle: i18n.popular,
  2165. viewElements: [header, prefix]
  2166. });
  2167. };
  2168. exports.extendedView = ({ messages }) => {
  2169. const header = div({ class: "tags-header" },
  2170. h2(i18n.extended),
  2171. p(i18n.extendedDescription)
  2172. );
  2173. return messageListView({
  2174. messages,
  2175. viewTitle: i18n.extended,
  2176. viewElements: header
  2177. });
  2178. };
  2179. exports.latestView = ({ messages }) => {
  2180. const header = div({ class: "tags-header" },
  2181. h2(i18n.latest),
  2182. p(i18n.latestDescription)
  2183. );
  2184. return messageListView({
  2185. messages,
  2186. viewTitle: i18n.latest,
  2187. viewElements: header
  2188. });
  2189. };
  2190. exports.topicsView = ({ messages, prefix }) => {
  2191. const header = div({ class: "tags-header" },
  2192. h2(i18n.topics),
  2193. p(i18n.topicsDescription)
  2194. );
  2195. return messageListView({
  2196. messages,
  2197. viewTitle: i18n.topics,
  2198. viewElements: [header, prefix]
  2199. });
  2200. };
  2201. exports.summaryView = ({ messages }) => {
  2202. const header = div({ class: "tags-header" },
  2203. h2(i18n.summaries),
  2204. p(i18n.summariesDescription)
  2205. );
  2206. return messageListView({
  2207. messages,
  2208. viewTitle: i18n.summaries,
  2209. viewElements: header,
  2210. aside: true
  2211. });
  2212. };
  2213. exports.spreadedView = ({ messages }) => {
  2214. const header = div({ class: "tags-header" },
  2215. h2(i18n.spreaded),
  2216. p(i18n.spreadedDescription)
  2217. );
  2218. return spreadedListView({
  2219. messages,
  2220. viewTitle: i18n.spreaded,
  2221. viewElements: header
  2222. });
  2223. };
  2224. exports.threadsView = ({ messages }) => {
  2225. const header = div({ class: "tags-header" },
  2226. h2(i18n.threads),
  2227. p(i18n.threadsDescription)
  2228. );
  2229. return messageListView({
  2230. messages,
  2231. viewTitle: i18n.threads,
  2232. viewElements: header,
  2233. aside: true
  2234. });
  2235. };
  2236. exports.previewSubtopicView = async ({
  2237. previewData,
  2238. messages,
  2239. myFeedId,
  2240. contentWarning,
  2241. }) => {
  2242. const publishAction = `/subtopic/${encodeURIComponent(messages[0].key)}`;
  2243. const preview = generatePreview({
  2244. previewData,
  2245. contentWarning,
  2246. action: publishAction,
  2247. });
  2248. return exports.subtopicView(
  2249. { messages, myFeedId },
  2250. preview,
  2251. previewData.text,
  2252. contentWarning
  2253. );
  2254. };
  2255. exports.subtopicView = async (
  2256. { messages, myFeedId },
  2257. preview,
  2258. text,
  2259. contentWarning
  2260. ) => {
  2261. const subtopicForm = `/subtopic/preview/${encodeURIComponent(
  2262. messages[messages.length - 1].key
  2263. )}`;
  2264. let markdownMention;
  2265. const messageElements = await Promise.all(
  2266. messages.reverse().map((message) => {
  2267. debug("%O", message);
  2268. const authorName = message.value.meta.author.name;
  2269. const authorFeedId = message.value.author;
  2270. if (authorFeedId !== myFeedId) {
  2271. if (message.key === messages[0].key) {
  2272. const x = `[@${authorName}](${authorFeedId})\n\n`;
  2273. markdownMention = x;
  2274. }
  2275. }
  2276. return post({ msg: message });
  2277. })
  2278. );
  2279. const authorName = messages[messages.length - 1].value.meta.author.name;
  2280. return template(
  2281. i18n.subtopicTitle({ authorName }),
  2282. div({ class: "thread-container" }, messageElements),
  2283. form(
  2284. { action: subtopicForm, method: "post", enctype: "multipart/form-data" },
  2285. i18n.blogSubject,
  2286. br,
  2287. label(
  2288. i18n.contentWarningLabel,
  2289. input({
  2290. name: "contentWarning",
  2291. type: "text",
  2292. class: "contentWarning",
  2293. value: contentWarning ? contentWarning : "",
  2294. placeholder: i18n.contentWarningPlaceholder,
  2295. })
  2296. ),
  2297. br,
  2298. label({ for: "text" }, i18n.blogMessage),
  2299. br,
  2300. textarea(
  2301. {
  2302. autofocus: true,
  2303. required: true,
  2304. name: "text",
  2305. rows: "6",
  2306. cols: "50",
  2307. placeholder: i18n.publishWarningPlaceholder,
  2308. },
  2309. text ? text : markdownMention
  2310. ),
  2311. br,
  2312. label(
  2313. { for: "blob" },
  2314. i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"
  2315. ),
  2316. input({ type: "file", id: "blob", name: "blob" }),
  2317. br,
  2318. br,
  2319. button({ type: "submit" }, i18n.blogPublish)
  2320. ),
  2321. preview ? div({ class: "comment-preview" }, preview) : ""
  2322. );
  2323. };