activity_view.js 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n } = require('./main_views');
  3. const moment = require("../server/node_modules/moment");
  4. const { renderUrl } = require('../backend/renderUrl');
  5. function capitalize(str) {
  6. return typeof str === 'string' && str.length ? str[0].toUpperCase() + str.slice(1) : '';
  7. }
  8. function sumAmounts(list = []) {
  9. return list.reduce((s, x) => s + (parseFloat(x.amount || 0) || 0), 0);
  10. }
  11. function pickActiveParliamentTerm(terms) {
  12. if (!terms.length) return null;
  13. const now = Date.now();
  14. const getC = t => t.value?.content || t.content || {};
  15. const isActive = t => {
  16. const c = getC(t);
  17. const s = new Date(c.startAt || 0).getTime();
  18. const e = new Date(c.endAt || 0).getTime();
  19. return s && e && s <= now && now < e;
  20. };
  21. const cmp = (a, b) => {
  22. const ca = getC(a);
  23. const cb = getC(b);
  24. const aAn = String(ca.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0;
  25. const bAn = String(cb.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0;
  26. if (aAn !== bAn) return aAn - bAn;
  27. const aC = new Date(ca.createdAt || ca.startAt || 0).getTime();
  28. const bC = new Date(cb.createdAt || cb.startAt || 0).getTime();
  29. if (aC !== bC) return aC - bC;
  30. const aS = new Date(ca.startAt || 0).getTime();
  31. const bS = new Date(cb.startAt || 0).getTime();
  32. if (aS !== bS) return aS - bS;
  33. return String(a.id || '').localeCompare(String(b.id || ''));
  34. };
  35. const active = terms.filter(isActive);
  36. if (active.length) return active.sort(cmp)[0];
  37. return terms.sort(cmp)[0];
  38. }
  39. function renderActionCards(actions, userId) {
  40. const validActions = actions
  41. .filter(action => {
  42. const content = action.value?.content || action.content;
  43. if (!content || typeof content !== 'object') return false;
  44. if (content.type === 'tombstone') return false;
  45. if (content.type === 'post' && content.private === true) return false;
  46. if (content.type === 'tribe' && content.isAnonymous === true) return false;
  47. if (content.type === 'task' && content.isPublic === "PRIVATE") return false;
  48. if (content.type === 'event' && content.isPublic === "private") return false;
  49. if (content.type === 'market') {
  50. if (content.stock === 0 && content.status !== 'SOLD') {
  51. return false;
  52. }
  53. }
  54. return true;
  55. })
  56. .sort((a, b) => b.ts - a.ts);
  57. const terms = validActions.filter(a => a.type === 'parliamentTerm');
  58. let chosenTerm = null;
  59. if (terms.length) chosenTerm = pickActiveParliamentTerm(terms);
  60. const deduped = chosenTerm
  61. ? validActions.filter(a => a.type !== 'parliamentTerm' || a === chosenTerm)
  62. : validActions;
  63. if (!deduped.length) {
  64. return div({ class: "no-actions" }, p(i18n.noActions));
  65. }
  66. const seenDocumentTitles = new Set();
  67. const cards = deduped.map(action => {
  68. const date = action.ts ? new Date(action.ts).toLocaleString() : "";
  69. const userLink = action.author
  70. ? a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author)
  71. : 'unknown';
  72. const type = action.type || 'unknown';
  73. let skip = false;
  74. let headerText;
  75. if (type.startsWith('parliament')) {
  76. const sub = type.replace('parliament', '');
  77. headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
  78. } else if (type.startsWith('courts')) {
  79. const rawSub = type.slice('courts'.length);
  80. const pretty = rawSub
  81. .replace(/^[_\s]+/, '')
  82. .replace(/[_\s]+/g, ' · ')
  83. .replace(/([a-z])([A-Z])/g, '$1 · $2');
  84. const finalSub = pretty || 'EVENT';
  85. headerText = `[COURTS · ${finalSub.toUpperCase()}]`;
  86. } else if (type === 'taskAssignment') {
  87. headerText = `[${String(i18n.typeTask || 'TASK').toUpperCase()} · ASSIGNMENT]`;
  88. } else {
  89. const typeLabel = i18n[`type${capitalize(type)}`] || type;
  90. headerText = `[${String(typeLabel).toUpperCase()}]`;
  91. }
  92. const content = action.value?.content || action.content || {};
  93. const cardBody = [];
  94. if (type === 'votes') {
  95. const { question, deadline, status, votes, totalVotes } = content;
  96. const commentCount =
  97. typeof action.commentCount === 'number'
  98. ? action.commentCount
  99. : (typeof content.commentCount === 'number' ? content.commentCount : 0);
  100. const votesList = votes && typeof votes === 'object'
  101. ? Object.entries(votes).map(([option, count]) => ({ option, count }))
  102. : [];
  103. cardBody.push(
  104. div({ class: 'card-section votes' },
  105. div(
  106. { class: 'card-field' },
  107. span({ class: 'card-label' }, i18n.question + ':'),
  108. span({ class: 'card-value' }, question)
  109. ),
  110. div(
  111. { class: 'card-field' },
  112. span({ class: 'card-label' }, i18n.deadline + ':'),
  113. span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')
  114. ),
  115. div(
  116. { class: 'card-field' },
  117. span({ class: 'card-label' }, i18n.voteTotalVotes + ':'),
  118. span({ class: 'card-value' }, totalVotes)
  119. ),
  120. div(
  121. { class: 'card-field' },
  122. span({ class: 'card-label' }, i18n.voteCommentsLabel + ':'),
  123. span({ class: 'card-value' }, String(commentCount))
  124. ),
  125. table(
  126. tr(...votesList.map(({ option }) => th(i18n[option] || option))),
  127. tr(...votesList.map(({ count }) => td(count)))
  128. )
  129. )
  130. );
  131. }
  132. if (type === 'transfer') {
  133. const { from, to, concept, amount, deadline, status, confirmedBy } = content;
  134. cardBody.push(
  135. div({ class: 'card-section transfer' },
  136. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)),
  137. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)),
  138. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
  139. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status))
  140. )
  141. );
  142. }
  143. if (type === 'bankWallet') {
  144. const { address } = content;
  145. cardBody.push(
  146. div({ class: 'card-section banking-wallet' },
  147. div({ class: 'card-field' },
  148. span({ class: 'card-label' }, i18n.bankWalletConnected + ':' ),
  149. span({ class: 'card-value' }, address)
  150. )
  151. )
  152. );
  153. }
  154. if (type === 'bankClaim') {
  155. const { amount, epochId, allocationId, txid } = content;
  156. const amt = Number(amount || 0);
  157. cardBody.push(
  158. div({ class: 'card-section banking-claim' },
  159. div({ class: 'card-field' },
  160. span({ class: 'card-label' }, i18n.bankUbiReceived + ':' ),
  161. span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`)
  162. ),
  163. epochId ? div({ class: 'card-field' },
  164. span({ class: 'card-label' }, i18n.bankEpochShort + ':' ),
  165. span({ class: 'card-value' }, epochId)
  166. ) : "",
  167. allocationId ? div({ class: 'card-field' },
  168. span({ class: 'card-label' }, i18n.bankAllocId + ':' ),
  169. span({ class: 'card-value' }, allocationId)
  170. ) : "",
  171. txid ? div({ class: 'card-field' },
  172. span({ class: 'card-label' }, i18n.bankTx + ':' ),
  173. a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid)
  174. ) : ""
  175. )
  176. );
  177. }
  178. if (type === 'pixelia') {
  179. const { author } = content;
  180. cardBody.push(
  181. div({ class: 'card-section pixelia' },
  182. div({ class: 'card-field' },
  183. a({ href: `/author/${encodeURIComponent(author)}`, class: 'activityVotePost' }, author)
  184. )
  185. )
  186. );
  187. }
  188. if (type === 'tribe') {
  189. const { title, image, description, location, tags, isLARP, inviteMode, isAnonymous, members } = content;
  190. const validTags = Array.isArray(tags) ? tags : [];
  191. cardBody.push(
  192. div({ class: 'card-section tribe' },
  193. h2({ class: 'tribe-title' },
  194. a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title)
  195. ),
  196. div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' },
  197. location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "",
  198. typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",
  199. inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "",
  200. typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : ""
  201. ),
  202. Array.isArray(members) ? h2(`${i18n.tribeMembersCount}: ${members.length}`) : "",
  203. image
  204. ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image tribe-image' })
  205. : img({ src: '/assets/images/default-tribe.png', class: 'feed-image tribe-image' }),
  206. p({ class: 'tribe-description' }, ...renderUrl(description || '')),
  207. validTags.length
  208. ? div({ class: 'card-tags' }, validTags.map(tag =>
  209. a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
  210. : ""
  211. )
  212. );
  213. }
  214. if (type === 'curriculum') {
  215. const { author, name, description, photo, personalSkills, oasisSkills, educationalSkills, languages, professionalSkills, status, preferences, createdAt, updatedAt} = content;
  216. cardBody.push(
  217. div({ class: 'card-section curriculum' },
  218. h2(a({ href: `/author/${encodeURIComponent(author)}`, class: "user-link" }, `@`, name)),
  219. div(
  220. { class: 'card-fields-container' },
  221. createdAt ?
  222. div(
  223. { class: 'card-field' },
  224. span({ class: 'card-label' }, i18n.cvCreatedAt + ':'),
  225. span({ class: 'card-value' }, moment(createdAt).format('YYYY-MM-DD HH:mm:ss'))
  226. )
  227. : "",
  228. updatedAt ?
  229. div(
  230. { class: 'card-field' },
  231. span({ class: 'card-label' }, i18n.cvUpdatedAt + ':'),
  232. span({ class: 'card-value' }, moment(updatedAt).format('YYYY-MM-DD HH:mm:ss'))
  233. )
  234. : ""
  235. ),
  236. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, status)) : "",
  237. preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvPreferencesLabel || 'Preferences') + ':'), span({ class: 'card-value' }, preferences)) : "",
  238. languages ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvLanguagesLabel || 'Languages') + ':'), span({ class: 'card-value' }, languages.toUpperCase())) : "",
  239. photo ?
  240. [
  241. br(),
  242. img({ class: "cv-photo", src: `/blob/${encodeURIComponent(photo)}` }),
  243. br()
  244. ]
  245. : "",
  246. p(...renderUrl(description || "")),
  247. personalSkills && personalSkills.length
  248. ? div({ class: 'card-tags' }, personalSkills.map(skill =>
  249. a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
  250. )) : "",
  251. oasisSkills && oasisSkills.length
  252. ? div({ class: 'card-tags' }, oasisSkills.map(skill =>
  253. a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
  254. )) : "",
  255. educationalSkills && educationalSkills.length
  256. ? div({ class: 'card-tags' }, educationalSkills.map(skill =>
  257. a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
  258. )) : "",
  259. professionalSkills && professionalSkills.length
  260. ? div({ class: 'card-tags' }, professionalSkills.map(skill =>
  261. a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
  262. )) : ""
  263. )
  264. );
  265. }
  266. if (type === 'image') {
  267. const { url } = content;
  268. cardBody.push(
  269. div({ class: 'card-section image' },
  270. img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image img-content' })
  271. )
  272. );
  273. }
  274. if (type === 'audio') {
  275. const { url, mimeType, title } = content;
  276. cardBody.push(
  277. div({ class: 'card-section audio' },
  278. title?.trim() ? h2({ class: 'audio-title' }, title) : "",
  279. url
  280. ? div({ class: "audio-container" },
  281. audioHyperaxe({
  282. controls: true,
  283. src: `/blob/${encodeURIComponent(url)}`,
  284. type: mimeType
  285. })
  286. )
  287. : p(i18n.audioNoFile)
  288. )
  289. );
  290. }
  291. if (type === 'video') {
  292. const { url, mimeType, title } = content;
  293. cardBody.push(
  294. div({ class: 'card-section video' },
  295. title?.trim() ? h2({ class: 'video-title' }, title) : "",
  296. url
  297. ? div({ class: "video-container" },
  298. videoHyperaxe({
  299. controls: true,
  300. src: `/blob/${encodeURIComponent(url)}`,
  301. type: mimeType,
  302. preload: 'metadata',
  303. width: '640',
  304. height: '360'
  305. })
  306. )
  307. : p(i18n.videoNoFile)
  308. )
  309. );
  310. }
  311. if (type === 'document') {
  312. const { url, title, key } = content;
  313. if (title && seenDocumentTitles.has(title.trim())) {
  314. return null;
  315. }
  316. if (title) seenDocumentTitles.add(title.trim());
  317. cardBody.push(
  318. div({ class: 'card-section document' },
  319. title?.trim() ? h2({ class: 'document-title' }, title) : "",
  320. div({
  321. id: `pdf-container-${key || url}`,
  322. class: 'pdf-viewer-container',
  323. 'data-pdf-url': `/blob/${encodeURIComponent(url)}`
  324. })
  325. )
  326. );
  327. }
  328. if (type === 'bookmark') {
  329. const { url } = content;
  330. cardBody.push(
  331. div({ class: 'card-section bookmark' },
  332. h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : "")
  333. )
  334. );
  335. }
  336. if (type === 'event') {
  337. const { title, description, date, location, price, attendees, organizer, isPublic } = content;
  338. cardBody.push(
  339. div({ class: 'card-section event' },
  340. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
  341. date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, new Date(date).toLocaleString())) : "",
  342. location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
  343. typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
  344. price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
  345. br(),
  346. organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), a({ class: "user-link", href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
  347. Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
  348. )
  349. );
  350. }
  351. if (type === 'task') {
  352. const { title, startTime, endTime, priority, status, author } = content;
  353. cardBody.push(
  354. div({ class: 'card-section task' },
  355. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
  356. priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.priority || 'Priority') + ':'), span({ class: 'card-value' }, priority)) : "",
  357. startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, new Date(startTime).toLocaleString())) : "",
  358. endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, new Date(endTime).toLocaleString())) : "",
  359. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : ""
  360. )
  361. );
  362. }
  363. if (type === 'taskAssignment') {
  364. const { title, added, removed } = content || {};
  365. const addList = Array.isArray(added) ? added : [];
  366. const remList = Array.isArray(removed) ? removed : [];
  367. const renderUserList = (ids) =>
  368. ids.map((id, i) => [i > 0 ? ', ' : '', a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id)]).flat();
  369. cardBody.push(
  370. div({ class: 'card-section task' },
  371. div({ class: 'card-field' },
  372. span({ class: 'card-label' }, i18n.title + ':'),
  373. span({ class: 'card-value' }, title || '')
  374. ),
  375. addList.length
  376. ? div({ class: 'card-field' },
  377. span({ class: 'card-label' }, (i18n.taskAssignedTo || 'Assigned to') + ':'),
  378. span({ class: 'card-value' }, ...renderUserList(addList))
  379. )
  380. : '',
  381. remList.length
  382. ? div({ class: 'card-field' },
  383. span({ class: 'card-label' }, (i18n.taskUnassignedFrom || 'Unassigned from') + ':'),
  384. span({ class: 'card-value' }, ...renderUserList(remList))
  385. )
  386. : ''
  387. )
  388. );
  389. }
  390. if (type === 'feed') {
  391. const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
  392. const { text, refeeds } = content;
  393. cardBody.push(
  394. div({ class: 'card-section feed' },
  395. div({ class: 'feed-text', innerHTML: renderTextWithStyles(text) }),
  396. h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-label' }, refeeds))
  397. )
  398. );
  399. }
  400. if (type === 'post') {
  401. const { contentWarning, text } = content || {};
  402. const rawText = text || '';
  403. const isHtml = typeof rawText === 'string' && /<\/?[a-z][\s\S]*>/i.test(rawText);
  404. let bodyNode;
  405. if (isHtml) {
  406. const hasAnchor = /<a\b[^>]*>/i.test(rawText);
  407. const linkified = hasAnchor
  408. ? rawText
  409. : rawText.replace(
  410. /(https?:\/\/[^\s<]+)/g,
  411. (url) =>
  412. `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
  413. );
  414. bodyNode = div({ class: 'post-text', innerHTML: linkified });
  415. } else {
  416. bodyNode = p({ class: 'post-text' }, ...renderUrl(rawText));
  417. }
  418. cardBody.push(
  419. div({ class: 'card-section post' },
  420. contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
  421. bodyNode
  422. )
  423. );
  424. }
  425. if (type === 'forum') {
  426. const { root, category, title, text, key, rootTitle, rootKey } = content;
  427. if (!root) {
  428. const linkKey = key || action.id;
  429. const linkText = (title && String(title).trim()) ? title : '(sin título)';
  430. cardBody.push(
  431. div({ class: 'card-section forum' },
  432. div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" },
  433. span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '),
  434. a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText)
  435. )
  436. )
  437. );
  438. } else {
  439. const rootId = typeof root === 'string' ? root : (root?.key || root?.id || '');
  440. const parentForum = actions.find(a => a.type === 'forum' && !a.content?.root && (a.id === rootId || a.content?.key === rootId));
  441. const parentTitle = (parentForum?.content?.title && String(parentForum.content.title).trim()) ? parentForum.content.title : ((rootTitle && String(rootTitle).trim()) ? rootTitle : '(sin título)');
  442. const hrefKey = rootKey || rootId;
  443. cardBody.push(
  444. div({ class: 'card-section forum' },
  445. div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" },
  446. span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '),
  447. a({ href: `/forum/${encodeURIComponent(hrefKey)}`, style: "font-weight:800;color:#4fc3f7;" }, parentTitle)
  448. ),
  449. br(),
  450. div({ class: 'card-field', style: 'margin-bottom:12px;' },
  451. p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text))
  452. )
  453. )
  454. );
  455. }
  456. }
  457. if (type === 'vote') {
  458. const { vote } = content;
  459. cardBody.push(
  460. div({ class: 'card-section vote' },
  461. p(
  462. a({ href: `/thread/${encodeURIComponent(vote.link)}#${encodeURIComponent(vote.link)}`, class: 'activityVotePost' }, vote.link)
  463. )
  464. )
  465. );
  466. }
  467. if (type === 'about') {
  468. const { about, name, image } = content;
  469. cardBody.push(
  470. div({ class: 'card-section about' },
  471. h2(a({ href: `/author/${encodeURIComponent(about)}`, class: "user-link" }, `@`, name)),
  472. image
  473. ? img({ src: `/blob/${encodeURIComponent(image)}` })
  474. : img({ src: '/assets/images/default-avatar.png', alt: name })
  475. )
  476. );
  477. }
  478. if (type === 'contact') {
  479. const { contact } = content;
  480. cardBody.push(
  481. div({ class: 'card-section contact' },
  482. p({ class: 'card-field' },
  483. a({ href: `/author/${encodeURIComponent(contact)}`, class: 'activitySpreadInhabitant2' }, contact)
  484. )
  485. )
  486. );
  487. }
  488. if (type === 'pub') {
  489. const { address } = content;
  490. const { host, key } = address || {};
  491. cardBody.push(
  492. div({ class: 'card-section pub' },
  493. p({ class: 'card-field' },
  494. a({ href: `/author/${encodeURIComponent(key || '')}`, class: 'activitySpreadInhabitant2' }, key || '')
  495. )
  496. )
  497. );
  498. }
  499. if (type === 'market') {
  500. const { item_type, title, price, status, deadline, stock, image, auctions_poll, seller } = content;
  501. const isSeller = seller && userId && seller === userId;
  502. cardBody.push(
  503. div({ class: 'card-section market' },
  504. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemTitle + ':'), span({ class: 'card-value' }, title)),
  505. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, item_type.toUpperCase())),
  506. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
  507. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
  508. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
  509. br(),
  510. image
  511. ? img({ src: `/blob/${encodeURIComponent(image)}` })
  512. : img({ src: '/assets/images/default-market.png', alt: title }),
  513. br(),
  514. div({ class: "market-card price" },
  515. p(`${i18n.marketItemPrice}: ${price} ECO`)
  516. ),
  517. item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
  518. ? div({ class: "auction-info" },
  519. auctions_poll && auctions_poll.length > 0
  520. ?
  521. table({ class: 'auction-bid-table' },
  522. tr(
  523. th(i18n.marketAuctionBidTime),
  524. th(i18n.marketAuctionUser),
  525. th(i18n.marketAuctionBidAmount)
  526. ),
  527. ...(auctions_poll || []).map(bid => {
  528. const [bidderId, bidAmount, bidTime] = bid.split(':');
  529. return tr(
  530. td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
  531. td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
  532. td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
  533. );
  534. })
  535. )
  536. : p(i18n.marketNoBids),
  537. form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
  538. input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
  539. br(),
  540. button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
  541. )
  542. ) : "",
  543. item_type === 'exchange' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
  544. ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` },
  545. button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
  546. ) : ""
  547. )
  548. );
  549. }
  550. if (type === 'report') {
  551. const { title, confirmations, severity, status } = content;
  552. cardBody.push(
  553. div({ class: 'card-section report' },
  554. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
  555. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "",
  556. severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.severity || 'Severity') + ':'), span({ class: 'card-value' }, severity.toUpperCase())) : "",
  557. Array.isArray(confirmations) ? h2({ class: 'card-label' }, (i18n.transfersConfirmations) + ': ' + confirmations.length) : ""
  558. )
  559. );
  560. }
  561. if (type === 'project') {
  562. const {
  563. title, status, progress, goal, pledged,
  564. deadline, followers, backers, milestones,
  565. bounty, bountyAmount, bounty_currency,
  566. activity, activityActor
  567. } = content;
  568. const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
  569. const displayStatus = String(status || 'ACTIVE').toUpperCase();
  570. const followersCount = Array.isArray(followers) ? followers.length : 0;
  571. const backersCount = Array.isArray(backers) ? backers.length : 0;
  572. const backersTotal = sumAmounts(backers || []);
  573. const msCount = Array.isArray(milestones) ? milestones.length : 0;
  574. const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
  575. const bountyVal = typeof bountyAmount !== 'undefined'
  576. ? bountyAmount
  577. : (typeof bounty === 'number' ? bounty : null);
  578. if (activity && activity.kind) {
  579. const tmpl =
  580. activity.kind === 'follow'
  581. ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
  582. : activity.kind === 'unfollow'
  583. ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
  584. : '%OASIS% performed an unknown action on %PROJECT%';
  585. const actionWord =
  586. activity.kind === 'follow'
  587. ? (i18n.following || 'FOLLOWING')
  588. : activity.kind === 'unfollow'
  589. ? (i18n.unfollowing || 'UNFOLLOWING')
  590. : 'ACTION';
  591. const msgHtml = tmpl
  592. .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
  593. .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
  594. .replace('%ACTION%', `<strong>${actionWord}</strong>`);
  595. return div({ class: 'card card-rpg' },
  596. div({ class: 'card-header' },
  597. h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
  598. form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
  599. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  600. )
  601. ),
  602. div(
  603. p({ innerHTML: msgHtml })
  604. ),
  605. p({ class: 'card-footer' },
  606. span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
  607. a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
  608. )
  609. );
  610. }
  611. cardBody.push(
  612. div({ class: 'card-section project' },
  613. title ? div({ class: 'card-field' },
  614. span({ class: 'card-label' }, i18n.title + ':'),
  615. span({ class: 'card-value' }, title)
  616. ) : "",
  617. typeof goal !== 'undefined' ? div({ class: 'card-field' },
  618. span({ class: 'card-label' }, i18n.projectGoal + ':'),
  619. span({ class: 'card-value' }, `${goal} ECO`)
  620. ) : "",
  621. typeof progress !== 'undefined' ? div({ class: 'card-field' },
  622. span({ class: 'card-label' }, i18n.projectProgress + ':'),
  623. span({ class: 'card-value' }, `${progress || 0}%`)
  624. ) : "",
  625. deadline ? div({ class: 'card-field' },
  626. span({ class: 'card-label' }, i18n.projectDeadline + ':'),
  627. span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
  628. ) : "",
  629. div({ class: 'card-field' },
  630. span({ class: 'card-label' }, i18n.projectStatus + ':'),
  631. span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
  632. ),
  633. div({ class: 'card-field' },
  634. span({ class: 'card-label' }, i18n.projectFunding + ':'),
  635. span({ class: 'card-value' }, `${ratio}%`)
  636. ),
  637. typeof pledged !== 'undefined' ? div({ class: 'card-field' },
  638. span({ class: 'card-label' }, i18n.projectPledged + ':'),
  639. span({ class: 'card-value' }, `${pledged || 0} ECO`)
  640. ) : "",
  641. div({ class: 'card-field' },
  642. span({ class: 'card-label' }, i18n.projectFollowers + ':'),
  643. span({ class: 'card-value' }, `${followersCount}`)
  644. ),
  645. div({ class: 'card-field' },
  646. span({ class: 'card-label' }, i18n.projectBackers + ':'),
  647. span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
  648. ),
  649. msCount ? div({ class: 'card-field' },
  650. span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
  651. span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
  652. ) : "",
  653. bountyVal != null ? div({ class: 'card-field' },
  654. span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
  655. span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
  656. ) : ""
  657. )
  658. );
  659. }
  660. if (type === 'aiExchange') {
  661. const { ctx } = content;
  662. cardBody.push(
  663. div({ class: 'card-section ai-exchange' },
  664. Array.isArray(ctx) && ctx.length
  665. ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiSnippetsLearned || 'Snippets learned') + ':'), span({ class: 'card-value' }, String(ctx.length)))
  666. : ""
  667. )
  668. );
  669. }
  670. if (type === 'karmaScore') {
  671. const { karmaScore } = content;
  672. cardBody.push(
  673. div({ class: 'card-section ai-exchange' },
  674. div({ class: 'card-field' },
  675. span({ class: 'card-label' }, i18n.bankingUserEngagementScore + ':'),
  676. span({ class: 'card-value' }, karmaScore)
  677. )
  678. )
  679. );
  680. }
  681. if (type === 'job') {
  682. const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content;
  683. cardBody.push(
  684. div({ class: 'card-section report' },
  685. div({ class: 'card-field' },
  686. span({ class: 'card-label' }, i18n.title + ':'),
  687. span({ class: 'card-value' }, title)
  688. ),
  689. salary && div({ class: 'card-field' },
  690. span({ class: 'card-label' }, i18n.jobSalary + ':'),
  691. span({ class: 'card-value' }, salary + ' ECO')
  692. ),
  693. status && div({ class: 'card-field' },
  694. span({ class: 'card-label' }, i18n.jobStatus + ':'),
  695. span({ class: 'card-value' }, status.toUpperCase())
  696. ),
  697. job_type && div({ class: 'card-field' },
  698. span({ class: 'card-label' }, i18n.jobType + ':'),
  699. span({ class: 'card-value' }, job_type.toUpperCase())
  700. ),
  701. location && div({ class: 'card-field' },
  702. span({ class: 'card-label' }, i18n.jobLocation + ':'),
  703. span({ class: 'card-value' }, location.toUpperCase())
  704. ),
  705. vacants && div({ class: 'card-field' },
  706. span({ class: 'card-label' }, i18n.jobVacants + ':'),
  707. span({ class: 'card-value' }, vacants)
  708. ),
  709. div({ class: 'card-field' },
  710. span({ class: 'card-label' }, i18n.jobSubscribers + ':'),
  711. span({ class: 'card-value' },
  712. Array.isArray(subscribers) && subscribers.length > 0
  713. ? `${subscribers.length}`
  714. : i18n.noSubscribers.toUpperCase()
  715. )
  716. )
  717. )
  718. );
  719. }
  720. if (type === 'parliamentCandidature') {
  721. const { targetType, targetId, targetTitle, method, votes, proposer } = content;
  722. const link = targetType === 'tribe'
  723. ? a({ href: `/tribe/${encodeURIComponent(targetId)}`, class: 'user-link' }, targetTitle || targetId)
  724. : a({ href: `/author/${encodeURIComponent(targetId)}`, class: 'user-link' }, targetId);
  725. const methodUpper = String(
  726. i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
  727. ).toUpperCase();
  728. cardBody.push(
  729. div({ class: 'card-section parliament' },
  730. div({ class: 'card-field' }, span({ class: 'card-label' }, (String(i18n.parliamentCandidatureId || 'Candidature').toUpperCase()) + ':'), span({ class: 'card-value' }, link)),
  731. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'METHOD') + ':'), span({ class: 'card-value' }, methodUpper)),
  732. typeof votes !== 'undefined'
  733. ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived || 'VOTES RECEIVED') + ':'), span({ class: 'card-value' }, String(votes)))
  734. : ''
  735. )
  736. );
  737. }
  738. if (type === 'parliamentTerm') {
  739. const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content;
  740. const powerTypeNorm = String(powerType || '').toLowerCase();
  741. const winnerLink =
  742. powerTypeNorm === 'tribe'
  743. ? a({ href: `/tribe/${encodeURIComponent(powerId)}`, class: 'user-link' }, powerTitle || powerId)
  744. : powerTypeNorm === 'none' || !powerTypeNorm
  745. ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY'))
  746. : a({ href: `/author/${encodeURIComponent(powerId)}`, class: 'user-link' }, powerTitle || powerId);
  747. const methodUpper = String(
  748. i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
  749. ).toUpperCase();
  750. cardBody.push(
  751. div({ class: 'card-section parliament' },
  752. startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '',
  753. endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '',
  754. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)),
  755. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
  756. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`))
  757. )
  758. );
  759. }
  760. if (type === 'parliamentProposal') {
  761. const { title, description, method, status, voteId, createdAt } = content;
  762. const methodUpper = String(
  763. i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
  764. ).toUpperCase();
  765. cardBody.push(
  766. div({ class: 'card-section parliament' },
  767. title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
  768. description ? p({ style: 'margin:.4rem 0' }, description) : '',
  769. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
  770. createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
  771. voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
  772. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
  773. )
  774. );
  775. }
  776. if (type === 'parliamentRevocation') {
  777. const { title, reasons, method, status, voteId, createdAt } = content;
  778. const methodUpper = String(
  779. i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
  780. ).toUpperCase();
  781. cardBody.push(
  782. div({ class: 'card-section parliament' },
  783. title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
  784. reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '',
  785. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
  786. createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
  787. voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
  788. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
  789. )
  790. );
  791. }
  792. if (type === 'parliamentLaw') {
  793. const { question, description, method, proposer, enactedAt, votes } = content;
  794. const yes = Number(votes?.YES || 0);
  795. const total = Number(votes?.total || votes?.TOTAL || 0);
  796. const methodUpper = String(
  797. i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
  798. ).toUpperCase();
  799. cardBody.push(
  800. div({ class: 'card-section parliament' },
  801. question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '',
  802. description ? p({ style: 'margin:.4rem 0' }, description) : '',
  803. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
  804. proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(proposer)}`, class: 'user-link' }, proposer))) : '',
  805. enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '',
  806. (total || yes) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawVotes || 'Votes') + ':'), span({ class: 'card-value' }, `${yes}/${total}`)) : ''
  807. )
  808. );
  809. }
  810. if (type.startsWith('courts')) {
  811. if (type === 'courtsCase') {
  812. const { title, method, accuser, status, answerBy, evidenceBy, decisionBy, needed, yes, total, voteId } = content;
  813. cardBody.push(
  814. div({ class: 'card-section courts' },
  815. title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '',
  816. status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '',
  817. method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '',
  818. answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '',
  819. evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '',
  820. decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '',
  821. accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(accuser)}`, class: 'user-link' }, accuser))) : '',
  822. typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '',
  823. (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '',
  824. voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsOpenVote + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : ''
  825. )
  826. );
  827. } else if (type === 'courtsNomination') {
  828. const { judgeId } = content;
  829. cardBody.push(
  830. div({ class: 'card-section courts' },
  831. judgeId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsJudge + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(judgeId)}`, class: 'user-link' }, judgeId))) : ''
  832. )
  833. );
  834. } else {
  835. skip = true;
  836. }
  837. }
  838. const viewHref = getViewDetailsAction(type, action);
  839. const isParliamentTarget =
  840. viewHref === '/parliament?filter=candidatures' ||
  841. viewHref === '/parliament?filter=government' ||
  842. viewHref === '/parliament?filter=proposals' ||
  843. viewHref === '/parliament?filter=revocations' ||
  844. viewHref === '/parliament?filter=laws';
  845. const isCourtsTarget =
  846. viewHref === '/courts?filter=cases' ||
  847. viewHref === '/courts?filter=mycases' ||
  848. viewHref === '/courts?filter=actions' ||
  849. viewHref === '/courts?filter=judges' ||
  850. viewHref === '/courts?filter=history' ||
  851. viewHref === '/courts?filter=rules' ||
  852. viewHref === '/courts?filter=open';
  853. const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
  854. const courtsFilter = isCourtsTarget ? (viewHref.split('filter=')[1] || '') : '';
  855. if (skip) {
  856. return null;
  857. }
  858. return div({ class: 'card card-rpg' },
  859. div({ class: 'card-header' },
  860. h2({ class: 'card-label' }, headerText),
  861. type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet' && (!action.tipId || action.tipId === action.id)
  862. ? (
  863. isParliamentTarget
  864. ? form(
  865. { method: "GET", action: "/parliament" },
  866. input({ type: "hidden", name: "filter", value: parliamentFilter }),
  867. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  868. )
  869. : isCourtsTarget
  870. ? form(
  871. { method: "GET", action: "/courts" },
  872. input({ type: "hidden", name: "filter", value: courtsFilter }),
  873. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  874. )
  875. : form(
  876. { method: "GET", action: viewHref },
  877. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  878. )
  879. )
  880. : ''
  881. ),
  882. div({ class: 'card-body' }, ...cardBody),
  883. p({ class: 'card-footer' },
  884. span({ class: 'date-link' }, `${date} ${i18n.performed} `),
  885. a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
  886. )
  887. );
  888. });
  889. const filteredCards = cards.filter(Boolean);
  890. if (!filteredCards.length) {
  891. return div({ class: "no-actions" }, p(i18n.noActions));
  892. }
  893. return filteredCards;
  894. }
  895. function getViewDetailsAction(type, action) {
  896. const id = encodeURIComponent(action.tipId || action.id);
  897. switch (type) {
  898. case 'parliamentCandidature': return `/parliament?filter=candidatures`;
  899. case 'parliamentTerm': return `/parliament?filter=government`;
  900. case 'parliamentProposal': return `/parliament?filter=proposals`;
  901. case 'parliamentRevocation': return `/parliament?filter=revocations`;
  902. case 'parliamentLaw': return `/parliament?filter=laws`;
  903. case 'courtsCase': return `/courts/cases/${encodeURIComponent(action.id)}`;
  904. case 'courtsEvidence': return `/courts?filter=actions`;
  905. case 'courtsAnswer': return `/courts?filter=actions`;
  906. case 'courtsVerdict': return `/courts?filter=actions`;
  907. case 'courtsSettlement': return `/courts?filter=actions`;
  908. case 'courtsSettlementProposal':return `/courts?filter=actions`;
  909. case 'courtsSettlementAccepted':return `/courts?filter=actions`;
  910. case 'courtsNomination': return `/courts?filter=judges`;
  911. case 'courtsNominationVote': return `/courts?filter=judges`;
  912. case 'votes': return `/votes/${id}`;
  913. case 'transfer': return `/transfers/${id}`;
  914. case 'pixelia': return `/pixelia`;
  915. case 'tribe': return `/tribe/${id}`;
  916. case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
  917. case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
  918. case 'image': return `/images/${id}`;
  919. case 'audio': return `/audios/${id}`;
  920. case 'video': return `/videos/${id}`;
  921. case 'forum': return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
  922. case 'document': return `/documents/${id}`;
  923. case 'bookmark': return `/bookmarks/${id}`;
  924. case 'event': return `/events/${id}`;
  925. case 'task': return `/tasks/${id}`;
  926. case 'taskAssignment': return `/tasks/${encodeURIComponent(action.content?.taskId || action.tipId || action.id)}`;
  927. case 'about': return `/author/${encodeURIComponent(action.author)}`;
  928. case 'post': return `/thread/${id}#${id}`;
  929. case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
  930. case 'contact': return `/inhabitants`;
  931. case 'pub': return `/invites`;
  932. case 'market': return `/market/${id}`;
  933. case 'job': return `/jobs/${id}`;
  934. case 'project': return `/projects/${id}`;
  935. case 'report': return `/reports/${id}`;
  936. case 'bankWallet': return `/wallet`;
  937. case 'bankClaim': return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
  938. default: return `/activity`;
  939. }
  940. }
  941. exports.activityView = (actions, filter, userId) => {
  942. const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity;
  943. const desc = i18n.activityDesc;
  944. const activityTypes = [
  945. { type: 'recent', label: i18n.typeRecent },
  946. { type: 'all', label: i18n.allButton },
  947. { type: 'mine', label: i18n.mineButton },
  948. { type: 'banking', label: i18n.typeBanking },
  949. { type: 'market', label: i18n.typeMarket },
  950. { type: 'project', label: i18n.typeProject },
  951. { type: 'job', label: i18n.typeJob },
  952. { type: 'transfer', label: i18n.typeTransfer },
  953. { type: 'parliament',label: i18n.typeParliament },
  954. { type: 'courts', label: i18n.typeCourts },
  955. { type: 'votes', label: i18n.typeVotes },
  956. { type: 'event', label: i18n.typeEvent },
  957. { type: 'task', label: i18n.typeTask },
  958. { type: 'report', label: i18n.typeReport },
  959. { type: 'tribe', label: i18n.typeTribe },
  960. { type: 'about', label: i18n.typeAbout },
  961. { type: 'curriculum',label: i18n.typeCurriculum },
  962. { type: 'karmaScore',label: i18n.typeKarmaScore },
  963. { type: 'feed', label: i18n.typeFeed },
  964. { type: 'aiExchange',label: i18n.typeAiExchange },
  965. { type: 'post', label: i18n.typePost },
  966. { type: 'pixelia', label: i18n.typePixelia },
  967. { type: 'forum', label: i18n.typeForum },
  968. { type: 'bookmark', label: i18n.typeBookmark },
  969. { type: 'image', label: i18n.typeImage },
  970. { type: 'video', label: i18n.typeVideo },
  971. { type: 'audio', label: i18n.typeAudio },
  972. { type: 'document', label: i18n.typeDocument }
  973. ];
  974. let filteredActions;
  975. if (filter === 'mine') {
  976. filteredActions = actions.filter(action => action.author === userId && action.type !== 'tombstone');
  977. } else if (filter === 'recent') {
  978. const now = Date.now();
  979. filteredActions = actions.filter(action => action.type !== 'tombstone' && action.ts && now - action.ts < 24 * 60 * 60 * 1000);
  980. } else if (filter === 'banking') {
  981. filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim'));
  982. } else if (filter === 'parliament') {
  983. filteredActions = actions.filter(action => ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(action.type));
  984. } else if (filter === 'courts') {
  985. filteredActions = actions.filter(action => {
  986. const t = String(action.type || '').toLowerCase();
  987. return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
  988. });
  989. } else if (filter === 'task') {
  990. filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment'));
  991. } else {
  992. filteredActions = actions.filter(action => (action.type === filter || filter === 'all') && action.type !== 'tombstone');
  993. }
  994. let html = template(
  995. title,
  996. section(
  997. div({ class: 'tags-header' },
  998. h2(i18n.activityList),
  999. p(desc)
  1000. ),
  1001. form({ method: 'GET', action: '/activity' },
  1002. div({ class: 'mode-buttons', style: 'display:grid; grid-template-columns: repeat(6, 1fr); gap: 16px; margin-bottom: 24px;' },
  1003. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1004. activityTypes.slice(0, 3).map(({ type, label }) =>
  1005. form({ method: 'GET', action: '/activity' },
  1006. input({ type: 'hidden', name: 'filter', value: type }),
  1007. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1008. )
  1009. )
  1010. ),
  1011. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1012. activityTypes.slice(3, 8).map(({ type, label }) =>
  1013. form({ method: 'GET', action: '/activity' },
  1014. input({ type: 'hidden', name: 'filter', value: type }),
  1015. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1016. )
  1017. )
  1018. ),
  1019. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1020. activityTypes.slice(8, 12).map(({ type, label }) =>
  1021. form({ method: 'GET', action: '/activity' },
  1022. input({ type: 'hidden', name: 'filter', value: type }),
  1023. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1024. )
  1025. )
  1026. ),
  1027. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1028. activityTypes.slice(12, 17).map(({ type, label }) =>
  1029. form({ method: 'GET', action: '/activity' },
  1030. input({ type: 'hidden', name: 'filter', value: type }),
  1031. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1032. )
  1033. )
  1034. ),
  1035. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1036. activityTypes.slice(17, 21).map(({ type, label }) =>
  1037. form({ method: 'GET', action: '/activity' },
  1038. input({ type: 'hidden', name: 'filter', value: type }),
  1039. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1040. )
  1041. )
  1042. ),
  1043. div({ style: 'display: flex; flex-direction: column; gap: 8px;' },
  1044. activityTypes.slice(21, 26).map(({ type, label }) =>
  1045. form({ method: 'GET', action: '/activity' },
  1046. input({ type: 'hidden', name: 'filter', value: type }),
  1047. button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
  1048. )
  1049. )
  1050. )
  1051. )
  1052. ),
  1053. section({ class: 'feed-container' }, renderActionCards(filteredActions, userId))
  1054. )
  1055. );
  1056. const hasDocument = actions.some(a => a && a.type === 'document');
  1057. if (hasDocument) {
  1058. html += `
  1059. <script type="module" src="/js/pdf.min.mjs"></script>
  1060. <script src="/js/pdf-viewer.js"></script>
  1061. `;
  1062. }
  1063. return html;
  1064. };