main_views.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704
  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 ssbClientGUI = require("../client/gui");
  10. const config = require("../server/ssb_config");
  11. const cooler = ssbClientGUI({ offline: config.offline });
  12. let ssb, userId;
  13. const getUserId = async () => {
  14. if (!ssb) ssb = await cooler.open();
  15. if (!userId) userId = ssb.id;
  16. return userId;
  17. };
  18. const { a, article, br, body, button, details, div, em, footer, form, h1, h2, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, textarea, title, tr, ul } = require("../server/node_modules/hyperaxe");
  19. const lodash = require("../server/node_modules/lodash");
  20. const markdown = require("./markdown");
  21. // set language
  22. const i18nBase = require("../client/assets/translations/i18n");
  23. let selectedLanguage = "en";
  24. let i18n = {};
  25. Object.assign(i18n, i18nBase[selectedLanguage]);
  26. exports.setLanguage = (language) => {
  27. selectedLanguage = language;
  28. const newLang = i18nBase[selectedLanguage] || i18nBase['en'];
  29. Object.keys(i18n).forEach(k => delete i18n[k]);
  30. Object.assign(i18n, newLang);
  31. };
  32. exports.i18n = i18n;
  33. exports.selectedLanguage = selectedLanguage;
  34. //markdown
  35. const markdownUrl = "https://commonmark.org/help/";
  36. const doctypeString = "<!DOCTYPE html>";
  37. const THREAD_PREVIEW_LENGTH = 3;
  38. const toAttributes = (obj) =>
  39. Object.entries(obj)
  40. .map(([key, val]) => `${key}=${val}`)
  41. .join(", ");
  42. const nbsp = "\xa0";
  43. const { getConfig } = require('../configs/config-manager.js');
  44. // menu INIT
  45. const navLink = ({ href, emoji, text, current }) =>
  46. li(
  47. a(
  48. { href, class: current ? "current" : "" },
  49. span({ class: "emoji" }, emoji),
  50. nbsp,
  51. text
  52. )
  53. );
  54. const customCSS = (filename) => {
  55. const customStyleFile = path.join(
  56. envPaths("oasis", { suffix: "" }).config,
  57. filename
  58. );
  59. try {
  60. if (fs.existsSync(customStyleFile)) {
  61. return link({ rel: "stylesheet", href: filename });
  62. }
  63. } catch (error) {
  64. return "";
  65. }
  66. };
  67. const renderPopularLink = () => {
  68. const popularMod = getConfig().modules.popularMod === 'on';
  69. if (popularMod) {
  70. return [
  71. navLink({ href: "/public/popular/day", emoji: "⌘", text: i18n.popular, class: "popular-link enabled" }),
  72. hr,
  73. ];
  74. }
  75. return '';
  76. };
  77. const renderTopicsLink = () => {
  78. const topicsMod = getConfig().modules.topicsMod === 'on';
  79. return topicsMod
  80. ? navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics, class: "topics-link enabled" })
  81. : '';
  82. };
  83. const renderSummariesLink = () => {
  84. const summariesMod = getConfig().modules.summariesMod === 'on';
  85. if (summariesMod) {
  86. return [
  87. navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries, class: "summaries-link enabled" }),
  88. ];
  89. }
  90. return '';
  91. };
  92. const renderLatestLink = () => {
  93. const latestMod = getConfig().modules.latestMod === 'on';
  94. return latestMod
  95. ? navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest, class: "latest-link enabled" })
  96. : '';
  97. };
  98. const renderThreadsLink = () => {
  99. const threadsMod = getConfig().modules.threadsMod === 'on';
  100. if (threadsMod) {
  101. return [
  102. navLink({ href: "/public/latest/threads", emoji: "♺", text: i18n.threads, class: "threads-link enabled" }),
  103. ];
  104. }
  105. return '';
  106. };
  107. const renderInvitesLink = () => {
  108. const invitesMod = getConfig().modules.invitesMod === 'on';
  109. return invitesMod
  110. ? navLink({ href: "/invites", emoji: "ꔹ", text: i18n.invites, class: "invites-link enabled" })
  111. : '';
  112. };
  113. const renderWalletLink = () => {
  114. const walletMod = getConfig().modules.walletMod === 'on';
  115. if (walletMod) {
  116. return [
  117. navLink({ href: "/wallet", emoji: "❄", text: i18n.wallet, class: "wallet-link enabled" }),
  118. ];
  119. }
  120. return '';
  121. };
  122. const renderLegacyLink = () => {
  123. const legacyMod = getConfig().modules.legacyMod === 'on';
  124. if (legacyMod) {
  125. return [
  126. navLink({ href: "/legacy", emoji: "ꖤ", text: i18n.legacy, class: "legacy-link enabled" }),
  127. ];
  128. }
  129. return '';
  130. };
  131. const renderCipherLink = () => {
  132. const cipherMod = getConfig().modules.cipherMod === 'on';
  133. if (cipherMod) {
  134. return [
  135. navLink({ href: "/cipher", emoji: "ꗄ", text: i18n.cipher, class: "cipher-link enabled" }),
  136. ];
  137. }
  138. return '';
  139. };
  140. const renderBookmarksLink = () => {
  141. const bookmarksMod = getConfig().modules.bookmarksMod === 'on';
  142. if (bookmarksMod) {
  143. return [
  144. hr(),
  145. navLink({ href: "/bookmarks", emoji: "ꔪ", text: i18n.bookmarksLabel, class: "bookmark-link enabled" }),
  146. ];
  147. }
  148. return '';
  149. };
  150. const renderImagesLink = () => {
  151. const imagesMod = getConfig().modules.imagesMod === 'on';
  152. if (imagesMod) {
  153. return [
  154. navLink({ href: "/images", emoji: "ꕥ", text: i18n.imagesLabel, class: "images-link enabled" }),
  155. ];
  156. }
  157. return '';
  158. };
  159. const renderVideosLink = () => {
  160. const videosMod = getConfig().modules.videosMod === 'on';
  161. if (videosMod) {
  162. return [
  163. navLink({ href: "/videos", emoji: "ꗟ", text: i18n.videosLabel, class: "videos-link enabled" }),
  164. ];
  165. }
  166. return '';
  167. };
  168. const renderAudiosLink = () => {
  169. const audiosMod = getConfig().modules.audiosMod === 'on';
  170. if (audiosMod) {
  171. return [
  172. navLink({ href: "/audios", emoji: "ꔿ", text: i18n.audiosLabel, class: "audios-link enabled" }),
  173. ];
  174. }
  175. return '';
  176. };
  177. const renderDocsLink = () => {
  178. const docsMod = getConfig().modules.docsMod === 'on';
  179. if (docsMod) {
  180. return [
  181. navLink({ href: "/documents", emoji: "ꕨ", text: i18n.docsLabel, class: "docs-link enabled" }),
  182. ];
  183. }
  184. return '';
  185. };
  186. const renderTagsLink = () => {
  187. const tagsMod = getConfig().modules.tagsMod === 'on';
  188. return tagsMod
  189. ? [
  190. navLink({ href: "/tags", emoji: "ꖶ", text: i18n.tagsLabel, class: "tags-link enabled" })
  191. ]
  192. : '';
  193. };
  194. const renderMultiverseLink = () => {
  195. const multiverseMod = getConfig().modules.multiverseMod === 'on';
  196. return multiverseMod
  197. ? [
  198. hr,
  199. navLink({ href: "/public/latest/extended", emoji: "∞", text: i18n.multiverse, class: "multiverse-link enabled" })
  200. ]
  201. : '';
  202. };
  203. const renderMarketLink = () => {
  204. const marketMod = getConfig().modules.marketMod === 'on';
  205. return marketMod
  206. ? [
  207. hr(),
  208. navLink({ href: "/market", emoji: "ꕻ", text: i18n.marketTitle }),
  209. ]
  210. : '';
  211. };
  212. const renderTribesLink = () => {
  213. const tribesMod = getConfig().modules.tribesMod === 'on';
  214. return tribesMod
  215. ? [
  216. navLink({ href: "/tribes", emoji: "ꖥ", text: i18n.tribesTitle, class: "tribes-link enabled" }),
  217. hr(),
  218. ]
  219. : '';
  220. };
  221. const renderGovernanceLink = () => {
  222. const governanceMod = getConfig().modules.governanceMod === 'on';
  223. return governanceMod
  224. ? [
  225. navLink({ href: "/votes", emoji: "ꔰ", text: i18n.governanceTitle, class: "votes-link enabled" }),
  226. ]
  227. : '';
  228. };
  229. const renderTrendingLink = () => {
  230. const trendingMod = getConfig().modules.trendingMod === 'on';
  231. return trendingMod
  232. ? [
  233. navLink({ href: "/trending", emoji: "ꗝ", text: i18n.trendingLabel, class: "trending-link enabled" }),
  234. ]
  235. : '';
  236. };
  237. const renderReportsLink = () => {
  238. const reportsMod = getConfig().modules.reportsMod === 'on';
  239. return reportsMod
  240. ? [
  241. navLink({ href: "/reports", emoji: "ꕥ", text: i18n.reportsTitle, class: "reports-link enabled" }),
  242. ]
  243. : '';
  244. };
  245. const renderOpinionsLink = () => {
  246. const opinionsMod = getConfig().modules.opinionsMod === 'on';
  247. return opinionsMod
  248. ? [
  249. navLink({ href: "/opinions", emoji: "ꔍ", text: i18n.opinionsTitle, class: "opinions-link enabled" }),
  250. ]
  251. : '';
  252. };
  253. const renderTransfersLink = () => {
  254. const transfersMod = getConfig().modules.transfersMod === 'on';
  255. return transfersMod
  256. ? [
  257. navLink({ href: "/transfers", emoji: "ꘉ", text: i18n.transfersTitle, class: "transfers-link enabled" }),
  258. ]
  259. : '';
  260. };
  261. const renderFeedLink = () => {
  262. const feedMod = getConfig().modules.feedMod === 'on';
  263. return feedMod
  264. ? [
  265. hr(),
  266. navLink({ href: "/feed", emoji: "ꕿ", text: i18n.feedTitle, class: "feed-link enabled" }),
  267. ]
  268. : '';
  269. };
  270. const renderPixeliaLink = () => {
  271. const pixeliaMod = getConfig().modules.pixeliaMod === 'on';
  272. return pixeliaMod
  273. ? [
  274. navLink({ href: "/pixelia", emoji: "ꔘ", text: i18n.pixeliaTitle, class: "pixelia-link enabled" }),
  275. ]
  276. : '';
  277. };
  278. const renderAgendaLink = () => {
  279. const agendaMod = getConfig().modules.agendaMod === 'on';
  280. return agendaMod
  281. ? [
  282. navLink({ href: "/agenda", emoji: "ꗤ", text: i18n.agendaTitle, class: "agenda-link enabled" }),
  283. ]
  284. : '';
  285. };
  286. const renderAILink = () => {
  287. const aiMod = getConfig().modules.aiMod === 'on';
  288. return aiMod
  289. ? [
  290. navLink({ href: "/ai", emoji: "ꘜ", text: i18n.ai, class: "ai-link enabled" }),
  291. ]
  292. : '';
  293. };
  294. const renderEventsLink = () => {
  295. const eventsMod = getConfig().modules.eventsMod === 'on';
  296. return eventsMod
  297. ? [
  298. navLink({ href: "/events", emoji: "ꕆ", text: i18n.eventsLabel, class: "events-link enabled" }),
  299. ]
  300. : '';
  301. };
  302. const renderTasksLink = () => {
  303. const tasksMod = getConfig().modules.tasksMod === 'on';
  304. return tasksMod
  305. ? [
  306. navLink({ href: "/tasks", emoji: "ꖧ", text: i18n.tasksTitle, class: "tasks-link enabled" }),
  307. ]
  308. : '';
  309. };
  310. const template = (titlePrefix, ...elements) => {
  311. const currentConfig = getConfig();
  312. const theme = currentConfig.themes.current || "Dark-SNH";
  313. const themeLink = link({
  314. rel: "stylesheet",
  315. href: `/assets/themes/${theme}.css`
  316. });
  317. const nodes = html(
  318. { lang: "en" },
  319. head(
  320. title(titlePrefix, " | Oasis"),
  321. link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
  322. themeLink,
  323. link({ rel: "icon", href: "/assets/images/favicon.svg" }),
  324. meta({ charset: "utf-8" }),
  325. meta({ name: "description", content: i18n.oasisDescription }),
  326. meta({ name: "viewport", content: toAttributes({ width: "device-width", "initial-scale": 1 }) })
  327. ),
  328. body(
  329. div(
  330. { class: "header" },
  331. div(
  332. { class: "top-bar-left" },
  333. a({ class: "logo-icon", href: "/" },
  334. img({ class: "logo-icon", src: "/assets/images/snh-oasis.jpg", alt: "Oasis Logo" })
  335. ),
  336. nav(
  337. ul(
  338. navLink({ href: "/profile", emoji: "⚉", text: i18n.profile }),
  339. navLink({ href: "/cv", emoji: "ꕛ", text: i18n.cvTitle }),
  340. renderLegacyLink(),
  341. renderWalletLink(),
  342. navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }),
  343. renderInvitesLink(),
  344. navLink({ href: "/modules", emoji: "ꗣ", text: i18n.modules }),
  345. navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
  346. )
  347. )
  348. ),
  349. div(
  350. { class: "top-bar-right" },
  351. nav(
  352. ul(
  353. renderCipherLink(),
  354. navLink({ href: "/pm", emoji: "ꕕ", text: i18n.privateMessage }),
  355. navLink({ href: "/publish", emoji: "❂", text: i18n.publish }),
  356. renderTagsLink(),
  357. renderAILink(),
  358. navLink({ href: "/search", emoji: "ꔅ", text: i18n.search })
  359. )
  360. ),
  361. )
  362. ),
  363. div(
  364. { class: "main-content" },
  365. div(
  366. { class: "sidebar-left" },
  367. nav(
  368. ul(
  369. navLink({ href: "/mentions", emoji: "✺", text: i18n.mentions }),
  370. navLink({ href: "/inbox", emoji: "☂", text: i18n.inbox }),
  371. renderAgendaLink(),
  372. navLink({ href: "/stats", emoji: "ꕷ", text: i18n.statistics }),
  373. hr,
  374. renderLatestLink(),
  375. renderThreadsLink(),
  376. renderTopicsLink(),
  377. renderSummariesLink(),
  378. renderPopularLink(),
  379. navLink({ href: "/inhabitants", emoji: "ꖘ", text: i18n.inhabitantsLabel }),
  380. renderTribesLink(),
  381. renderGovernanceLink(),
  382. renderEventsLink(),
  383. renderTasksLink(),
  384. renderReportsLink(),
  385. renderMultiverseLink()
  386. )
  387. )
  388. ),
  389. main({ id: "content", class: "main-column" }, elements),
  390. div(
  391. { class: "sidebar-right" },
  392. nav(
  393. ul(
  394. navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }),
  395. renderTrendingLink(),
  396. renderOpinionsLink(),
  397. renderFeedLink(),
  398. renderPixeliaLink(),
  399. renderMarketLink(),
  400. renderTransfersLink(),
  401. renderBookmarksLink(),
  402. renderImagesLink(),
  403. renderVideosLink(),
  404. renderAudiosLink(),
  405. renderDocsLink(),
  406. )
  407. )
  408. ),
  409. )
  410. )
  411. );
  412. return doctypeString + nodes.outerHTML;
  413. };
  414. // menu END
  415. exports.template = template;
  416. const thread = (messages) => {
  417. let lookingForTarget = true;
  418. let shallowest = Infinity;
  419. for (let i = messages.length - 1; i >= 0; i--) {
  420. const msg = messages[i];
  421. const depth = lodash.get(msg, "value.meta.thread.depth", 0);
  422. if (lookingForTarget) {
  423. const isThreadTarget = Boolean(
  424. lodash.get(msg, "value.meta.thread.target", false)
  425. );
  426. if (isThreadTarget) {
  427. lookingForTarget = false;
  428. }
  429. } else {
  430. if (depth < shallowest) {
  431. lodash.set(msg, "value.meta.thread.ancestorOfTarget", true);
  432. shallowest = depth;
  433. }
  434. }
  435. }
  436. const msgList = [];
  437. for (let i = 0; i < messages.length; i++) {
  438. const j = i + 1;
  439. const currentMsg = messages[i];
  440. const nextMsg = messages[j];
  441. const depth = (msg) => {
  442. if (msg === undefined) return 0;
  443. return lodash.get(msg, "value.meta.thread.depth", 0);
  444. };
  445. msgList.push(post({ msg: currentMsg }));
  446. if (depth(currentMsg) < depth(nextMsg)) {
  447. const isAncestor = Boolean(
  448. lodash.get(currentMsg, "value.meta.thread.ancestorOfTarget", false)
  449. );
  450. const isBlocked = Boolean(nextMsg.value.meta.blocking);
  451. const nextAuthor = lodash.get(nextMsg, "value.meta.author.name");
  452. const nextSnippet = postSnippet(
  453. lodash.has(nextMsg, "value.content.contentWarning")
  454. ? lodash.get(nextMsg, "value.content.contentWarning")
  455. : lodash.get(nextMsg, "value.content.text")
  456. );
  457. msgList.push(
  458. details(
  459. isAncestor ? { open: true } : {},
  460. summary(
  461. isBlocked
  462. ? i18n.relationshipBlockingPost
  463. : `${nextAuthor}: ${nextSnippet}`
  464. )
  465. )
  466. );
  467. } else if (depth(currentMsg) > depth(nextMsg)) {
  468. const diffDepth = depth(currentMsg) - depth(nextMsg);
  469. }
  470. }
  471. return div({ class: "thread-container" }, ...msgList);
  472. };
  473. const postSnippet = (text) => {
  474. const max = 40;
  475. text = text.trim().split("\n", 3).join("\n");
  476. text = text.replace(/_|`|\*|#|^\[@.*?]|\[|]|\(\S*?\)/g, "").trim();
  477. text = text.replace(/:$/, "");
  478. text = text.trim().split("\n", 1)[0].trim();
  479. if (text.length > max) {
  480. text = text.substring(0, max - 1) + "…";
  481. }
  482. return text;
  483. };
  484. const continueThreadComponent = (thread, isComment) => {
  485. const encoded = {
  486. next: encodeURIComponent(thread[THREAD_PREVIEW_LENGTH + 1].key),
  487. parent: encodeURIComponent(thread[0].key),
  488. };
  489. const left = thread.length - (THREAD_PREVIEW_LENGTH + 1);
  490. let continueLink;
  491. if (isComment == false) {
  492. continueLink = `/thread/${encoded.parent}#${encoded.next}`;
  493. return a(
  494. { href: continueLink },
  495. i18n.continueReading, ` ${left} `, i18n.moreComments+`${left === 1 ? "" : "s"}`
  496. );
  497. } else {
  498. continueLink = `/thread/${encoded.parent}`;
  499. return a({ href: continueLink }, i18n.readThread);
  500. }
  501. };
  502. const postAside = ({ key, value }) => {
  503. const thread = value.meta.thread;
  504. if (thread == null) return null;
  505. const isComment = value.meta.postType === "comment";
  506. let postsToShow;
  507. if (isComment) {
  508. const commentPosition = thread.findIndex((msg) => msg.key === key);
  509. postsToShow = thread.slice(
  510. commentPosition + 1,
  511. Math.min(commentPosition + (THREAD_PREVIEW_LENGTH + 1), thread.length)
  512. );
  513. } else {
  514. postsToShow = thread.slice(
  515. 1,
  516. Math.min(thread.length, THREAD_PREVIEW_LENGTH + 1)
  517. );
  518. }
  519. const fragments = postsToShow.map((p) => post({ msg: p }));
  520. if (thread.length > THREAD_PREVIEW_LENGTH + 1) {
  521. fragments.push(section(continueThreadComponent(thread, isComment)));
  522. }
  523. return fragments;
  524. };
  525. const post = ({ msg, aside = false, preview = false }) => {
  526. const encoded = {
  527. key: encodeURIComponent(msg.key),
  528. author: encodeURIComponent(msg.value?.author),
  529. parent: encodeURIComponent(msg.value?.content?.root),
  530. };
  531. const url = {
  532. author: `/author/${encoded.author}`,
  533. likeForm: `/like/${encoded.key}`,
  534. link: `/thread/${encoded.key}#${encoded.key}`,
  535. parent: `/thread/${encoded.parent}#${encoded.parent}`,
  536. avatar: msg.value?.meta?.author?.avatar?.url || '/assets/images/default-avatar.png',
  537. json: `/json/${encoded.key}`,
  538. subtopic: `/subtopic/${encoded.key}`,
  539. comment: `/comment/${encoded.key}`,
  540. };
  541. const isPrivate = Boolean(msg.value?.meta?.private);
  542. const isBlocked = Boolean(msg.value?.meta?.blocking);
  543. const isRoot = msg.value?.content?.root == null;
  544. const isFork = msg.value?.meta?.postType === "subtopic";
  545. const hasContentWarning = typeof msg.value?.content?.contentWarning === "string";
  546. const isThreadTarget = Boolean(lodash.get(msg, "value.meta.thread.target", false));
  547. const { name } = msg.value?.meta?.author || { name: "Anonymous" };
  548. const markdownContent = msg.value?.content?.text;
  549. const emptyContent = "<p>undefined</p>\n";
  550. const articleElement =
  551. markdownContent === emptyContent
  552. ? article(
  553. { class: "content" },
  554. pre({
  555. innerHTML: highlightJs.highlight(
  556. JSON.stringify(msg, null, 2),
  557. { language: "json", ignoreIllegals: true }
  558. ).value,
  559. })
  560. )
  561. : article({ class: "content", innerHTML: markdownContent });
  562. if (preview) {
  563. return section(
  564. { id: msg.key, class: "post-preview" },
  565. hasContentWarning
  566. ? details(summary(msg.value?.content?.contentWarning), articleElement)
  567. : articleElement
  568. );
  569. }
  570. const ts_received = msg.value?.meta?.timestamp?.received;
  571. if (!ts_received || !ts_received.iso8601 || !moment(ts_received.iso8601, moment.ISO_8601, true).isValid()) {
  572. return null;
  573. }
  574. const validTimestamp = moment(ts_received.iso8601, moment.ISO_8601);
  575. const timeAgo = validTimestamp.fromNow();
  576. const timeAbsolute = validTimestamp.toISOString().split(".")[0].replace("T", " ");
  577. const likeButton = msg.value?.meta?.voted
  578. ? { value: 0, class: "liked" }
  579. : { value: 1, class: null };
  580. const likeCount = msg.value?.meta?.votes?.length || 0;
  581. const maxLikedNameLength = 16;
  582. const maxLikedNames = 16;
  583. const likedByNames = msg.value?.meta?.votes
  584. .slice(0, maxLikedNames)
  585. .map((person) => person.name)
  586. .map((name) => name.slice(0, maxLikedNameLength))
  587. .join(", ");
  588. const additionalLikesMessage =
  589. likeCount > maxLikedNames ? `+${likeCount - maxLikedNames} more` : ``;
  590. const likedByMessage =
  591. likeCount > 0 ? `${likedByNames} ${additionalLikesMessage}` : null;
  592. const messageClasses = ["post"];
  593. const recps = [];
  594. const addRecps = (recpsInfo) => {
  595. recpsInfo.forEach((recp) => {
  596. recps.push(
  597. a(
  598. { href: `/author/${encodeURIComponent(recp.feedId)}` },
  599. img({ class: "avatar", src: recp.avatarUrl, alt: "" })
  600. )
  601. );
  602. });
  603. };
  604. if (isPrivate) {
  605. messageClasses.push("private");
  606. addRecps(msg.value?.meta?.recpsInfo || []);
  607. }
  608. if (isThreadTarget) {
  609. messageClasses.push("thread-target");
  610. }
  611. if (isBlocked) {
  612. messageClasses.push("blocked");
  613. return section(
  614. {
  615. id: msg.key,
  616. class: messageClasses.join(" "),
  617. },
  618. i18n.relationshipBlockingPost
  619. );
  620. }
  621. const postOptions = {
  622. post: null,
  623. comment: i18n.commentDescription({ parentUrl: url.parent }),
  624. subtopic: i18n.subtopicDescription({ parentUrl: url.parent }),
  625. mystery: i18n.mysteryDescription,
  626. };
  627. const articleContent = article(
  628. { class: "content" },
  629. hasContentWarning ? div({ class: "post-subject" }, msg.value?.content?.contentWarning) : null,
  630. articleElement
  631. );
  632. const fragment = section(
  633. {
  634. id: msg.key,
  635. class: messageClasses.join(" "),
  636. },
  637. header(
  638. div(
  639. { class: "header-content" },
  640. a(
  641. { href: url.author },
  642. img({ class: "avatar-profile", src: url.avatar, alt: "" })
  643. ),
  644. span({ class: "created-at" }, `${i18n.createdBy} `, a({ href: url.author }, "@", name), ` | ${timeAbsolute} | ${i18n.sendTime} `, a({ href: url.link }, timeAgo), ` ${i18n.timeAgo}`),
  645. isPrivate ? "🔒" : null,
  646. isPrivate ? recps : null
  647. )
  648. ),
  649. articleContent,
  650. footer(
  651. div(
  652. form(
  653. { action: url.likeForm, method: "post" },
  654. button(
  655. {
  656. name: "voteValue",
  657. type: "submit",
  658. value: likeButton.value,
  659. class: likeButton.class,
  660. title: likedByMessage,
  661. },
  662. `☉ ${likeCount}`
  663. )
  664. ),
  665. a({ href: url.comment }, i18n.comment),
  666. isPrivate || isRoot || isFork
  667. ? null
  668. : a({ href: url.subtopic }, nbsp, i18n.subtopic)
  669. ),
  670. br()
  671. )
  672. );
  673. const threadSeparator = [br()];
  674. if (aside) {
  675. return [fragment, postAside(msg), isRoot ? threadSeparator : null];
  676. } else {
  677. return fragment;
  678. }
  679. };
  680. exports.editProfileView = ({ name, description }) =>
  681. template(
  682. i18n.editProfile,
  683. section(
  684. h1(i18n.editProfile),
  685. p(i18n.editProfileDescription),
  686. form(
  687. {
  688. action: "/profile/edit",
  689. method: "POST",
  690. enctype: "multipart/form-data",
  691. },
  692. label(
  693. i18n.profileImage,
  694. br,
  695. input({ type: "file", name: "image", accept: "image/*" })
  696. ),
  697. br,br,
  698. label(i18n.profileName,
  699. br,
  700. input({ name: "name", value: name })),
  701. br,br,
  702. label(
  703. i18n.profileDescription,
  704. br,
  705. textarea(
  706. {
  707. autofocus: true,
  708. name: "description",
  709. },
  710. description
  711. )
  712. ),
  713. br,
  714. button(
  715. {
  716. type: "submit",
  717. },
  718. i18n.submit
  719. )
  720. )
  721. )
  722. );
  723. exports.authorView = ({
  724. avatarUrl,
  725. description,
  726. feedId,
  727. messages,
  728. firstPost,
  729. lastPost,
  730. name,
  731. relationship,
  732. }) => {
  733. const mention = `[@${name}](${feedId})`;
  734. const markdownMention = highlightJs.highlight(mention, { language: "markdown", ignoreIllegals: true }).value;
  735. const contactForms = [];
  736. const addForm = ({ action }) =>
  737. contactForms.push(
  738. form(
  739. {
  740. action: `/${action}/${encodeURIComponent(feedId)}`,
  741. method: "post",
  742. },
  743. button(
  744. {
  745. type: "submit",
  746. },
  747. i18n[action]
  748. )
  749. )
  750. );
  751. if (relationship.me === false) {
  752. if (relationship.following) {
  753. addForm({ action: "unfollow" });
  754. } else if (relationship.blocking) {
  755. addForm({ action: "unblock" });
  756. } else {
  757. addForm({ action: "follow" });
  758. addForm({ action: "block" });
  759. }
  760. }
  761. const relationshipMessage = (() => {
  762. if (relationship.me) return i18n.relationshipYou;
  763. const following = relationship.following === true;
  764. const followsMe = relationship.followsMe === true;
  765. if (following && followsMe) {
  766. return i18n.relationshipMutuals;
  767. }
  768. const messages = [];
  769. messages.push(
  770. following
  771. ? i18n.relationshipFollowing
  772. : i18n.relationshipNone
  773. );
  774. messages.push(
  775. followsMe
  776. ? i18n.relationshipTheyFollow
  777. : i18n.relationshipNotFollowing
  778. );
  779. return messages.join(". ") + ".";
  780. })();
  781. const prefix = section(
  782. { class: "message" },
  783. div(
  784. { class: "profile" },
  785. div({ class: "avatar-container" },
  786. img({ class: "avatar", src: avatarUrl }),
  787. h1({ class: "name" }, name),
  788. ),
  789. pre({
  790. class: "md-mention",
  791. innerHTML: markdownMention,
  792. })
  793. ),
  794. description !== "" ? article({ innerHTML: markdown(description) }) : null,
  795. footer(
  796. div(
  797. { class: "profile" },
  798. ...contactForms.map(form => span({ style: "font-weight: bold;" }, form)),
  799. relationship.me ? (
  800. span({ class: "status you" }, i18n.relationshipYou)
  801. ) : (
  802. div({ class: "relationship-status" },
  803. relationship.blocking && relationship.blockedBy
  804. ? span({ class: "status blocked" }, i18n.relationshipMutualBlock)
  805. : [
  806. relationship.blocking
  807. ? span({ class: "status blocked" }, i18n.relationshipBlocking)
  808. : null,
  809. relationship.blockedBy
  810. ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy)
  811. : null,
  812. relationship.following && relationship.followsMe
  813. ? span({ class: "status mutual" }, i18n.relationshipMutuals)
  814. : [
  815. span(
  816. { class: "status supporting" },
  817. relationship.following
  818. ? i18n.relationshipFollowing
  819. : i18n.relationshipNone
  820. ),
  821. span(
  822. { class: "status supported-by" },
  823. relationship.followsMe
  824. ? i18n.relationshipTheyFollow
  825. : i18n.relationshipNotFollowing
  826. )
  827. ]
  828. ]
  829. )
  830. ),
  831. relationship.me
  832. ? a({ href: `/profile/edit`, class: "btn" }, nbsp, i18n.editProfile)
  833. : null,
  834. a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes)
  835. )
  836. )
  837. );
  838. const linkUrl = relationship.me
  839. ? "/profile/"
  840. : `/author/${encodeURIComponent(feedId)}/`;
  841. let items = messages.map((msg) => post({ msg }));
  842. if (items.length === 0) {
  843. if (lastPost === undefined) {
  844. items.push(section(div(span(i18n.feedEmpty))));
  845. } else {
  846. items.push(
  847. section(
  848. div(
  849. span(i18n.feedRangeEmpty),
  850. a({ href: `${linkUrl}` }, i18n.seeFullFeed)
  851. )
  852. )
  853. );
  854. }
  855. } else {
  856. const highestSeqNum = messages[0].value.sequence;
  857. const lowestSeqNum = messages[messages.length - 1].value.sequence;
  858. const newerPostsLink = a(
  859. {
  860. href:
  861. lastPost !== undefined && highestSeqNum < lastPost.value.sequence
  862. ? `${linkUrl}?gt=${highestSeqNum}`
  863. : "#",
  864. class:
  865. lastPost !== undefined && highestSeqNum < lastPost.value.sequence
  866. ? "btn"
  867. : "btn disabled",
  868. "aria-disabled":
  869. lastPost === undefined || highestSeqNum >= lastPost.value.sequence
  870. },
  871. i18n.newerPosts
  872. );
  873. const olderPostsLink = a(
  874. {
  875. href:
  876. lowestSeqNum > firstPost.value.sequence
  877. ? `${linkUrl}?lt=${lowestSeqNum}`
  878. : "#",
  879. class:
  880. lowestSeqNum > firstPost.value.sequence
  881. ? "btn"
  882. : "btn disabled",
  883. "aria-disabled": !(lowestSeqNum > firstPost.value.sequence)
  884. },
  885. i18n.olderPosts
  886. );
  887. const pagination = section(
  888. { class: "message" },
  889. footer(div(newerPostsLink, olderPostsLink), br())
  890. );
  891. items.unshift(pagination);
  892. items.push(pagination);
  893. }
  894. return template(i18n.profile, prefix, items);
  895. };
  896. exports.previewCommentView = async ({
  897. previewData,
  898. messages,
  899. myFeedId,
  900. parentMessage,
  901. contentWarning,
  902. }) => {
  903. if (!parentMessage || !parentMessage.value) {
  904. throw new Error("Missing parentMessage or value");
  905. }
  906. const publishAction = `/comment/${encodeURIComponent(messages[0].key)}`;
  907. const preview = generatePreview({
  908. previewData,
  909. contentWarning,
  910. action: publishAction,
  911. });
  912. return exports.commentView(
  913. { messages, myFeedId, parentMessage },
  914. preview,
  915. previewData.text,
  916. contentWarning
  917. );
  918. };
  919. exports.commentView = async (
  920. { messages, myFeedId, parentMessage },
  921. preview,
  922. text,
  923. contentWarning
  924. ) => {
  925. let markdownMention;
  926. const authorName = parentMessage?.value?.meta?.author?.name || "Anonymous";
  927. const messageElements = await Promise.all(
  928. messages.reverse().map(async (message) => {
  929. const isRootMessage = message.key === parentMessage.key;
  930. const messageAuthorName = message.value?.meta?.author?.name || "Anonymous";
  931. const authorFeedId = myFeedId;
  932. if (authorFeedId !== myFeedId) {
  933. if (message.key === parentMessage.key) {
  934. const x = `[@${messageAuthorName}](${authorFeedId})\n\n`;
  935. markdownMention = x;
  936. }
  937. }
  938. const timestamp = message?.value?.meta?.timestamp?.received;
  939. const validTimestamp = moment(timestamp, moment.ISO_8601, true);
  940. const timeAgo = validTimestamp.isValid()
  941. ? validTimestamp.fromNow()
  942. : "Invalid time";
  943. const messageId = message.key.endsWith('.sha256') ? message.key.slice(0, -7) : message.key;
  944. const result = await post({ msg: { ...message, key: messageId } });
  945. return result;
  946. })
  947. );
  948. const action = `/comment/preview/${encodeURIComponent(messages[0].key)}`;
  949. const method = "post";
  950. const isPrivate = parentMessage?.value?.meta?.private;
  951. const publicOrPrivate = isPrivate ? i18n.commentPrivate : i18n.commentPublic;
  952. const maybeSubtopicText = isPrivate ? [null] : i18n.commentWarning;
  953. return template(
  954. i18n.commentTitle({ authorName }),
  955. div({ class: "thread-container" }, messageElements),
  956. form(
  957. { action, method, enctype: "multipart/form-data" },
  958. i18n.blogSubject,
  959. br,
  960. label(
  961. i18n.contentWarningLabel,
  962. input({
  963. name: "contentWarning",
  964. type: "text",
  965. class: "contentWarning",
  966. value: contentWarning ? contentWarning : "",
  967. placeholder: i18n.contentWarningPlaceholder,
  968. })
  969. ),
  970. br,
  971. label({ for: "text" }, i18n.blogMessage),
  972. br,
  973. textarea(
  974. {
  975. autofocus: true,
  976. required: true,
  977. name: "text",
  978. rows: "6",
  979. cols: "50",
  980. placeholder: i18n.publishWarningPlaceholder,
  981. },
  982. text ? text : isPrivate ? null : markdownMention
  983. ),
  984. br,
  985. label(
  986. { for: "blob" },
  987. i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"
  988. ),
  989. input({ type: "file", id: "blob", name: "blob" }),
  990. br,
  991. br,
  992. button({ type: "submit" }, i18n.blogPublish)
  993. ),
  994. preview ? div({ class: "comment-preview" }, preview) : ""
  995. );
  996. };
  997. const renderMessage = (msg) => {
  998. const content = lodash.get(msg, "value.content", {});
  999. const author = msg.value.author || "Anonymous";
  1000. const createdAt = new Date(msg.value.timestamp).toLocaleString();
  1001. const mentionsText = content.text || '';
  1002. return div({ class: "mention-item" }, [
  1003. div({ class: "mention-content", innerHTML: mentionsText || '[No content]' }),
  1004. p(a({ class: 'user-link', href: `/author/${encodeURIComponent(author)}` }, author)),
  1005. p(`${i18n.createdAtLabel || i18n.mentionsCreatedAt}: ${createdAt}`)
  1006. ]);
  1007. };
  1008. exports.mentionsView = ({ messages, myFeedId }) => {
  1009. const title = i18n.mentions;
  1010. const description = i18n.mentionsDescription;
  1011. if (!Array.isArray(messages) || messages.length === 0) {
  1012. return template(
  1013. title,
  1014. section(
  1015. div({ class: "tags-header" },
  1016. h2(title),
  1017. p(description)
  1018. )
  1019. ),
  1020. section(
  1021. div({ class: "mentions-list" },
  1022. p({ class: "empty" }, i18n.noMentions)
  1023. )
  1024. )
  1025. );
  1026. }
  1027. const filteredMessages = messages.filter(msg => {
  1028. const mentions = lodash.get(msg, "value.content.mentions", {});
  1029. return Object.keys(mentions).length > 0;
  1030. });
  1031. if (filteredMessages.length === 0) {
  1032. return template(
  1033. title,
  1034. section(
  1035. div({ class: "tags-header" },
  1036. h2(title),
  1037. p(description)
  1038. )
  1039. ),
  1040. section(
  1041. div({ class: "mentions-list" },
  1042. p({ class: "empty" }, i18n.noMentions)
  1043. )
  1044. )
  1045. );
  1046. }
  1047. return template(
  1048. title,
  1049. section(
  1050. div({ class: "tags-header" },
  1051. h2(title),
  1052. p(description)
  1053. )
  1054. ),
  1055. section(
  1056. div({ class: "mentions-list" },
  1057. filteredMessages.map(renderMessage)
  1058. )
  1059. )
  1060. );
  1061. };
  1062. exports.privateView = async (input, filter) => {
  1063. const messages = Array.isArray(input) ? input : input.messages;
  1064. const userId = await getUserId();
  1065. const counts = {
  1066. inbox: messages.filter(m => m.value.content.to?.includes(userId)).length,
  1067. sent: messages.filter(m => m.value.content.from === userId).length
  1068. };
  1069. const filtered =
  1070. filter === 'sent' ? messages.filter(m => m.value.content.from === userId) :
  1071. filter === 'inbox' ? messages.filter(m => m.value.content.to?.includes(userId)) :
  1072. messages;
  1073. return template(
  1074. i18n.private,
  1075. section(
  1076. div({ class: 'tags-header' },
  1077. h2(i18n.private),
  1078. p(i18n.privateDescription)
  1079. ),
  1080. div({ class: 'filters' },
  1081. form({ method: 'GET', action: '/inbox' }, [
  1082. button({
  1083. type: 'submit',
  1084. name: 'filter',
  1085. value: 'inbox',
  1086. class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn'
  1087. }, i18n.privateInbox),
  1088. button({
  1089. type: 'submit',
  1090. name: 'filter',
  1091. value: 'sent',
  1092. class: filter === 'sent' ? 'filter-btn active' : 'filter-btn'
  1093. }, i18n.privateSent),
  1094. button({
  1095. type: 'submit',
  1096. name: 'filter',
  1097. value: 'create',
  1098. class: 'create-button',
  1099. formaction: '/pm',
  1100. formmethod: 'GET'
  1101. }, i18n.pmCreateButton)
  1102. ])
  1103. ),
  1104. div({ class: 'message-list' },
  1105. filtered.length
  1106. ? filtered.map(msg => {
  1107. const content = msg?.value?.content;
  1108. const author = msg?.value?.author;
  1109. if (!content || !author) {
  1110. return div({ class: 'malformed-message' }, 'Invalid message');
  1111. }
  1112. const subject = content.subject || '(no subject)';
  1113. const text = content.text || '';
  1114. const sentAt = new Date(content.sentAt || msg.timestamp).toLocaleString();
  1115. const from = content.from;
  1116. const toLinks = (content.to || []).map(addr =>
  1117. a({ class: 'user-link', href: `/author/${encodeURIComponent(addr)}` }, addr)
  1118. );
  1119. return div({ class: 'message-item' },
  1120. p(subject),
  1121. div({ class: 'message-text' }, text),
  1122. p({ class: 'card-footer' },
  1123. span({ class: 'date-link' }, `${sentAt} ${i18n.performed} `),
  1124. a({ href: `/author/${encodeURIComponent(from)}`, class: 'user-link' }, `${from}`)
  1125. ),
  1126. form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(msg.key)}`, class: 'delete-message-form' },
  1127. button({ type: 'submit', class: 'delete-btn' }, i18n.privateDelete)
  1128. )
  1129. );
  1130. })
  1131. : p({ class: 'empty' }, i18n.noPrivateMessages)
  1132. )
  1133. )
  1134. );
  1135. };
  1136. exports.publishCustomView = async () => {
  1137. const action = "/publish/custom";
  1138. const method = "post";
  1139. return template(
  1140. i18n.publishCustom,
  1141. section(
  1142. div({ class: "tags-header" },
  1143. h2(i18n.publishCustom),
  1144. p(i18n.publishCustomDescription)
  1145. ),
  1146. form(
  1147. { action, method },
  1148. textarea(
  1149. {
  1150. autofocus: true,
  1151. required: true,
  1152. name: "text",
  1153. rows: 10,
  1154. style: "width: 100%;"
  1155. },
  1156. "{\n",
  1157. ' "type": "feed",\n',
  1158. ' "hello": "world"\n',
  1159. "}"
  1160. ),
  1161. br,
  1162. br,
  1163. button({ type: "submit" }, i18n.submit)
  1164. )
  1165. ),
  1166. section(
  1167. div({ class: "tags-header" },
  1168. p(i18n.publishBasicInfo({ href: "/publish" }))
  1169. )
  1170. )
  1171. );
  1172. };
  1173. exports.threadView = ({ messages }) => {
  1174. const rootMessage = messages[0];
  1175. const rootAuthorName = rootMessage.value.meta.author.name;
  1176. const rootSnippet = postSnippet(
  1177. lodash.get(rootMessage, "value.content.text", i18n.mysteryDescription)
  1178. );
  1179. return template([`@${rootAuthorName}`],
  1180. div(
  1181. thread(messages)
  1182. )
  1183. );
  1184. };
  1185. exports.publishView = (preview, text, contentWarning) => {
  1186. return template(
  1187. i18n.publish,
  1188. section(
  1189. div({ class: "tags-header" },
  1190. h2(i18n.publishBlog),
  1191. p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" }))
  1192. )
  1193. ),
  1194. section(
  1195. div({ class: "publish-form" },
  1196. form(
  1197. {
  1198. action: "/publish/preview",
  1199. method: "post",
  1200. enctype: "multipart/form-data",
  1201. },
  1202. [
  1203. label({ for: "contentWarning" }, i18n.blogSubject),
  1204. br(),
  1205. input({
  1206. name: "contentWarning",
  1207. id: "contentWarning",
  1208. type: "text",
  1209. class: "contentWarning",
  1210. value: contentWarning || "",
  1211. placeholder: i18n.contentWarningPlaceholder
  1212. }),
  1213. br(),
  1214. label({ for: "text" }, i18n.blogMessage),
  1215. br(),
  1216. textarea(
  1217. {
  1218. required: true,
  1219. name: "text",
  1220. id: "text",
  1221. rows: "6",
  1222. cols: "50",
  1223. placeholder: i18n.publishWarningPlaceholder,
  1224. class: "publish-textarea"
  1225. },
  1226. text || ""
  1227. ),
  1228. br(),
  1229. label({ for: "blob" }, i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"),
  1230. br(),
  1231. input({ type: "file", id: "blob", name: "blob" }),
  1232. br(), br(),
  1233. button({ type: "submit" }, i18n.blogPublish)
  1234. ]
  1235. )
  1236. )
  1237. ),
  1238. preview || "",
  1239. section(
  1240. div({ class: "tags-header" },
  1241. p(i18n.publishCustomInfo({ href: "/publish/custom" }))
  1242. )
  1243. )
  1244. );
  1245. };
  1246. const generatePreview = ({ previewData, contentWarning, action }) => {
  1247. const { authorMeta, formattedText, mentions } = previewData;
  1248. const renderedText = formattedText;
  1249. const msg = {
  1250. key: "%non-existent.preview",
  1251. value: {
  1252. author: authorMeta.id,
  1253. content: {
  1254. type: "post",
  1255. text: renderedText,
  1256. mentions: mentions,
  1257. },
  1258. timestamp: Date.now(),
  1259. meta: {
  1260. isPrivate: false,
  1261. votes: [],
  1262. author: {
  1263. name: authorMeta.name,
  1264. avatar: {
  1265. url: `http://localhost:3000/blob/${encodeURIComponent(authorMeta.image)}`,
  1266. },
  1267. },
  1268. },
  1269. },
  1270. };
  1271. if (contentWarning) {
  1272. msg.value.content.contentWarning = contentWarning;
  1273. }
  1274. if (msg.value.meta.author.avatar.url === 'http://localhost:3000/blob/%260000000000000000000000000000000000000000000%3D.sha256') {
  1275. msg.value.meta.author.avatar.url = '/assets/images/default-avatar.png';
  1276. }
  1277. const ts = new Date(msg.value.timestamp);
  1278. lodash.set(msg, "value.meta.timestamp.received.iso8601", ts.toISOString());
  1279. const ago = Date.now() - Number(ts);
  1280. const prettyAgo = prettyMs(ago, { compact: true });
  1281. lodash.set(msg, "value.meta.timestamp.received.since", prettyAgo);
  1282. return div(
  1283. section(
  1284. { class: "post-preview" },
  1285. div(
  1286. { class: "preview-content" },
  1287. h2(i18n.messagePreview),
  1288. post({ msg, preview: true })
  1289. ),
  1290. ),
  1291. section(
  1292. { class: "mention-suggestions" },
  1293. Object.keys(mentions).map((name) => {
  1294. const matches = mentions[name];
  1295. return div(
  1296. h2(i18n.mentionsMatching),
  1297. { class: "mention-card" },
  1298. a(
  1299. {
  1300. href: `/author/@${encodeURIComponent(matches[0].feed)}`,
  1301. },
  1302. img({ src: msg.value.meta.author.avatar.url, class: "avatar-profile" })
  1303. ),
  1304. br,
  1305. div(
  1306. { class: "mention-name" },
  1307. span({ class: "label" }, `${i18n.mentionsName}: `),
  1308. a(
  1309. {
  1310. href: `/author/@${encodeURIComponent(matches[0].feed)}`,
  1311. },
  1312. `@${authorMeta.name}`
  1313. )
  1314. ),
  1315. div(
  1316. { class: "mention-relationship" },
  1317. span({ class: "label" }, `${i18n.mentionsRelationship}:`),
  1318. span({ class: "relationship" }, matches[0].rel.followsMe ? i18n.relationshipMutuals : i18n.relationshipNotMutuals),
  1319. { class: "mention-relationship-details" },
  1320. span({ class: "emoji" }, matches[0].rel.followsMe ? "☍" : "⚼"),
  1321. span({ class: "mentions-listing" },
  1322. a({ class: 'user-link', href: `/author/@${encodeURIComponent(matches[0].feed)}` }, `@${matches[0].feed}`)
  1323. )
  1324. )
  1325. );
  1326. })
  1327. ),
  1328. section(
  1329. form(
  1330. { action, method: "post" },
  1331. [
  1332. input({ type: "hidden", name: "text", value: renderedText }), // Pass the formatted text
  1333. input({ type: "hidden", name: "contentWarning", value: contentWarning || "" }),
  1334. input({ type: "hidden", name: "mentions", value: JSON.stringify(mentions) }),
  1335. button({ type: "submit" }, i18n.publish)
  1336. ]
  1337. )
  1338. )
  1339. );
  1340. };
  1341. exports.previewView = ({ previewData, contentWarning }) => {
  1342. const publishAction = "/publish";
  1343. const preview = generatePreview({
  1344. previewData,
  1345. contentWarning,
  1346. action: publishAction,
  1347. });
  1348. return exports.publishView(preview, previewData.formattedText, contentWarning);
  1349. };
  1350. const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => {
  1351. if (!viewTitle && !viewDescription) {
  1352. return null;
  1353. }
  1354. return section(
  1355. { class: "viewInfo" },
  1356. viewTitle ? h1(viewTitle) : null,
  1357. viewDescription ? em(viewDescription) : null
  1358. );
  1359. };
  1360. exports.likesView = async ({ messages, feed, name }) => {
  1361. const authorLink = a(
  1362. { href: `/author/${encodeURIComponent(feed)}` },
  1363. "@" + name
  1364. );
  1365. return template(
  1366. ["@", name],
  1367. viewInfoBox({
  1368. viewTitle: span(authorLink),
  1369. viewDescription: span(i18n.spreadedDescription)
  1370. }),
  1371. messages.map((msg) => post({ msg }))
  1372. );
  1373. };
  1374. const messageListView = ({
  1375. messages,
  1376. viewTitle = null,
  1377. viewDescription = null,
  1378. viewElements = null,
  1379. aside = null,
  1380. }) => {
  1381. const hasHeader = !!viewElements;
  1382. const titleBlock = hasHeader
  1383. ? viewElements
  1384. : div({ class: "tags-header" },
  1385. h2(viewTitle),
  1386. p(viewDescription)
  1387. );
  1388. return template(
  1389. viewTitle,
  1390. section(titleBlock),
  1391. messages.map((msg) => post({ msg, aside }))
  1392. );
  1393. };
  1394. exports.popularView = ({ messages, prefix }) => {
  1395. const header = div({ class: "tags-header" },
  1396. h2(i18n.popular),
  1397. p(i18n.popularDescription)
  1398. );
  1399. return messageListView({
  1400. messages,
  1401. viewTitle: i18n.popular,
  1402. viewElements: [header, prefix]
  1403. });
  1404. };
  1405. exports.extendedView = ({ messages }) => {
  1406. const header = div({ class: "tags-header" },
  1407. h2(i18n.extended),
  1408. p(i18n.extendedDescription)
  1409. );
  1410. return messageListView({
  1411. messages,
  1412. viewTitle: i18n.extended,
  1413. viewElements: header
  1414. });
  1415. };
  1416. exports.latestView = ({ messages }) => {
  1417. const header = div({ class: "tags-header" },
  1418. h2(i18n.latest),
  1419. p(i18n.latestDescription)
  1420. );
  1421. return messageListView({
  1422. messages,
  1423. viewTitle: i18n.latest,
  1424. viewElements: header
  1425. });
  1426. };
  1427. exports.topicsView = ({ messages, prefix }) => {
  1428. const header = div({ class: "tags-header" },
  1429. h2(i18n.topics),
  1430. p(i18n.topicsDescription)
  1431. );
  1432. return messageListView({
  1433. messages,
  1434. viewTitle: i18n.topics,
  1435. viewElements: [header, prefix]
  1436. });
  1437. };
  1438. exports.summaryView = ({ messages }) => {
  1439. const header = div({ class: "tags-header" },
  1440. h2(i18n.summaries),
  1441. p(i18n.summariesDescription)
  1442. );
  1443. return messageListView({
  1444. messages,
  1445. viewTitle: i18n.summaries,
  1446. viewElements: header,
  1447. aside: true
  1448. });
  1449. };
  1450. exports.spreadedView = ({ messages }) => {
  1451. const header = div({ class: "tags-header" },
  1452. h2(i18n.spreaded),
  1453. p(i18n.spreadedDescription)
  1454. );
  1455. return spreadedListView({
  1456. messages,
  1457. viewTitle: i18n.spreaded,
  1458. viewElements: header
  1459. });
  1460. };
  1461. exports.threadsView = ({ messages }) => {
  1462. const header = div({ class: "tags-header" },
  1463. h2(i18n.threads),
  1464. p(i18n.threadsDescription)
  1465. );
  1466. return messageListView({
  1467. messages,
  1468. viewTitle: i18n.threads,
  1469. viewElements: header,
  1470. aside: true
  1471. });
  1472. };
  1473. exports.previewSubtopicView = async ({
  1474. previewData,
  1475. messages,
  1476. myFeedId,
  1477. contentWarning,
  1478. }) => {
  1479. const publishAction = `/subtopic/${encodeURIComponent(messages[0].key)}`;
  1480. const preview = generatePreview({
  1481. previewData,
  1482. contentWarning,
  1483. action: publishAction,
  1484. });
  1485. return exports.subtopicView(
  1486. { messages, myFeedId },
  1487. preview,
  1488. previewData.text,
  1489. contentWarning
  1490. );
  1491. };
  1492. exports.subtopicView = async (
  1493. { messages, myFeedId },
  1494. preview,
  1495. text,
  1496. contentWarning
  1497. ) => {
  1498. const subtopicForm = `/subtopic/preview/${encodeURIComponent(
  1499. messages[messages.length - 1].key
  1500. )}`;
  1501. let markdownMention;
  1502. const messageElements = await Promise.all(
  1503. messages.reverse().map((message) => {
  1504. debug("%O", message);
  1505. const authorName = message.value.meta.author.name;
  1506. const authorFeedId = message.value.author;
  1507. if (authorFeedId !== myFeedId) {
  1508. if (message.key === messages[0].key) {
  1509. const x = `[@${authorName}](${authorFeedId})\n\n`;
  1510. markdownMention = x;
  1511. }
  1512. }
  1513. return post({ msg: message });
  1514. })
  1515. );
  1516. const authorName = messages[messages.length - 1].value.meta.author.name;
  1517. return template(
  1518. i18n.subtopicTitle({ authorName }),
  1519. div({ class: "thread-container" }, messageElements),
  1520. form(
  1521. { action: subtopicForm, method: "post", enctype: "multipart/form-data" },
  1522. i18n.blogSubject,
  1523. br,
  1524. label(
  1525. i18n.contentWarningLabel,
  1526. input({
  1527. name: "contentWarning",
  1528. type: "text",
  1529. class: "contentWarning",
  1530. value: contentWarning ? contentWarning : "",
  1531. placeholder: i18n.contentWarningPlaceholder,
  1532. })
  1533. ),
  1534. br,
  1535. label({ for: "text" }, i18n.blogMessage),
  1536. br,
  1537. textarea(
  1538. {
  1539. autofocus: true,
  1540. required: true,
  1541. name: "text",
  1542. rows: "6",
  1543. cols: "50",
  1544. placeholder: i18n.publishWarningPlaceholder,
  1545. },
  1546. text ? text : markdownMention
  1547. ),
  1548. br,
  1549. label(
  1550. { for: "blob" },
  1551. i18n.blogImage || "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)"
  1552. ),
  1553. input({ type: "file", id: "blob", name: "blob" }),
  1554. br,
  1555. br,
  1556. button({ type: "submit" }, i18n.blogPublish)
  1557. ),
  1558. preview ? div({ class: "comment-preview" }, preview) : ""
  1559. );
  1560. };