|
@@ -21,7 +21,7 @@ const getUserId = async () => {
|
|
|
return userId;
|
|
|
};
|
|
|
|
|
|
-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, strong } = require("../server/node_modules/hyperaxe");
|
|
|
+const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, textarea, title, tr, ul, strong } = require("../server/node_modules/hyperaxe");
|
|
|
|
|
|
const lodash = require("../server/node_modules/lodash");
|
|
|
const markdown = require("./markdown");
|
|
@@ -40,7 +40,7 @@ exports.setLanguage = (language) => {
|
|
|
exports.i18n = i18n;
|
|
|
exports.selectedLanguage = selectedLanguage;
|
|
|
|
|
|
-//markdown
|
|
|
+// markdown
|
|
|
const markdownUrl = "https://commonmark.org/help/";
|
|
|
|
|
|
const doctypeString = "<!DOCTYPE html>";
|
|
@@ -1239,269 +1239,297 @@ exports.mentionsView = ({ messages, myFeedId }) => {
|
|
|
};
|
|
|
|
|
|
exports.privateView = async (messagesInput, filter) => {
|
|
|
- const messages = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages;
|
|
|
- const userId = await getUserId();
|
|
|
+ const messagesRaw = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages
|
|
|
+ const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true)
|
|
|
+ const userId = await getUserId()
|
|
|
|
|
|
- const filtered =
|
|
|
- filter === 'sent' ? messages.filter(m => m.value.content.from === userId) :
|
|
|
- filter === 'inbox' ? messages.filter(m => m.value.content.to?.includes(userId)) :
|
|
|
- messages;
|
|
|
+ const isSent = m => (m?.value?.author === userId) || (m?.value?.content?.from === userId)
|
|
|
+ const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId)
|
|
|
|
|
|
- const linkAuthor = (id) =>
|
|
|
- a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id);
|
|
|
+ const linkAuthor = (id) =>
|
|
|
+ a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id)
|
|
|
|
|
|
- const hrefFor = {
|
|
|
- job: (id) => `/jobs/${encodeURIComponent(id)}`,
|
|
|
- project: (id) => `/projects/${encodeURIComponent(id)}`,
|
|
|
- market: (id) => `/market/${encodeURIComponent(id)}`
|
|
|
- };
|
|
|
+ const hrefFor = {
|
|
|
+ job: (id) => `/jobs/${encodeURIComponent(id)}`,
|
|
|
+ project: (id) => `/projects/${encodeURIComponent(id)}`,
|
|
|
+ market: (id) => `/market/${encodeURIComponent(id)}`
|
|
|
+ }
|
|
|
|
|
|
- const clickableCardProps = (href, extraClass = '') => {
|
|
|
- const props = { class: `pm-card ${extraClass}` };
|
|
|
- if (href) {
|
|
|
- props.onclick = `window.location='${href}'`;
|
|
|
- props.tabindex = 0;
|
|
|
- props.onkeypress = `if(event.key==='Enter') window.location='${href}'`;
|
|
|
- }
|
|
|
- return props;
|
|
|
- };
|
|
|
+ const clickableCardProps = (href, extraClass = '') => {
|
|
|
+ const props = { class: `pm-card ${extraClass}` }
|
|
|
+ if (href) {
|
|
|
+ props.onclick = `window.location='${href}'`
|
|
|
+ props.tabindex = 0
|
|
|
+ props.onkeypress = `if(event.key==='Enter') window.location='${href}'`
|
|
|
+ }
|
|
|
+ return props
|
|
|
+ }
|
|
|
|
|
|
- const chip = (txt) => span({ class: 'chip' }, txt);
|
|
|
+ const chip = (txt) => span({ class: 'chip' }, txt)
|
|
|
|
|
|
- function header({ sentAt, from, toLinks, botIcon = '', botLabel = '' }) {
|
|
|
- return div({ class: 'pm-header' },
|
|
|
- span({ class: 'date-link' }, `${moment(sentAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`),
|
|
|
- botIcon || botLabel
|
|
|
- ? span({ class: 'pm-from' }, `${botIcon} ${botLabel}`)
|
|
|
- : [
|
|
|
- span({ class: 'pm-from' }, i18n.pmFromLabel + ' ', linkAuthor(from)),
|
|
|
- span({ class: 'pm-to' }, i18n.pmToLabel + ' ', toLinks)
|
|
|
- ]
|
|
|
- );
|
|
|
- }
|
|
|
+ function headerLine({ sentAt, from, toLinks, textLen }) {
|
|
|
+ return div({ class: 'pm-header' },
|
|
|
+ span({ class: 'date-link' }, `${moment(sentAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`),
|
|
|
+ span({ class: 'pm-from' }, ' ', i18n.pmFromLabel, ' ', linkAuthor(from)),
|
|
|
+ span({ class: 'pm-to' }, ' ', '→', ' ', i18n.pmToLabel, ' ', toLinks)
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- function actions({ key, replyId }) {
|
|
|
- const stop = { onclick: 'event.stopPropagation()' };
|
|
|
- return div({ class: 'pm-actions' },
|
|
|
- form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'delete-message-form', style: 'display:inline-block;margin-right:8px;', ...stop },
|
|
|
- button({ type: 'submit', class: 'delete-btn' }, i18n.privateDelete)
|
|
|
- ),
|
|
|
- form({ method: 'GET', action: '/pm', style: 'display:inline-block;', ...stop },
|
|
|
- input({ type: 'hidden', name: 'recipients', value: replyId }),
|
|
|
- button({ type: 'submit', class: 'reply-btn' }, i18n.pmCreateButton)
|
|
|
- )
|
|
|
- );
|
|
|
- }
|
|
|
+ function actions({ key, replyId, subjectRaw, text }) {
|
|
|
+ const stop = { onclick: 'event.stopPropagation()' }
|
|
|
+ const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}`
|
|
|
+ return div({ class: 'pm-actions' },
|
|
|
+ form({ method: 'GET', action: '/pm', class: 'pm-action-form', ...stop },
|
|
|
+ input({ type: 'hidden', name: 'recipients', value: replyId }),
|
|
|
+ input({ type: 'hidden', name: 'subject', value: subjectReply }),
|
|
|
+ input({ type: 'hidden', name: 'quote', value: text || '' }),
|
|
|
+ button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase())
|
|
|
+ ),
|
|
|
+ form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop },
|
|
|
+ button({ type: 'submit', class: 'pm-btn delete-btn' }, i18n.privateDelete.toUpperCase())
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- function quoted(str) {
|
|
|
- const m = str.match(/"([^"]+)"/);
|
|
|
- return m ? m[1] : '';
|
|
|
- }
|
|
|
+ function canonicalSubject(s) {
|
|
|
+ return (s || '').replace(/^\s*(RE:\s*)+/i, '').trim()
|
|
|
+ }
|
|
|
|
|
|
- function pickLink(str, kind) {
|
|
|
- if (kind === 'job') {
|
|
|
- const m = str.match(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/);
|
|
|
- return m ? m[1] : '';
|
|
|
- }
|
|
|
- if (kind === 'project') {
|
|
|
- const m = str.match(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/);
|
|
|
- return m ? m[1] : '';
|
|
|
- }
|
|
|
- if (kind === 'market') {
|
|
|
- const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/);
|
|
|
- return m ? m[1] : '';
|
|
|
- }
|
|
|
- return '';
|
|
|
- }
|
|
|
+ function participantsKey(m) {
|
|
|
+ const c = m?.value?.content || {}
|
|
|
+ const set = new Set([m?.value?.author, ...(Array.isArray(c.to) ? c.to : [])])
|
|
|
+ return Array.from(set).sort().join('|')
|
|
|
+ }
|
|
|
|
|
|
- function JobCard({ type, sentAt, from, toLinks, text, key }) {
|
|
|
- const isSub = type === 'JOB_SUBSCRIBED';
|
|
|
- const icon = isSub ? '🟡' : '🟠';
|
|
|
- const titleH = isSub ? (i18n.inboxJobSubscribedTitle || 'New subscription to your job offer') : (i18n.inboxJobUnsubscribedTitle || 'Unsubscription from your job offer');
|
|
|
- const jobTitle = quoted(text) || 'job';
|
|
|
- const jobId = pickLink(text, 'job');
|
|
|
- const href = jobId ? hrefFor.job(jobId) : null;
|
|
|
- return div(
|
|
|
- clickableCardProps(href, `job-notification ${isSub ? 'job-sub' : 'job-unsub'}`),
|
|
|
- header({ sentAt, from, toLinks, botIcon: icon, botLabel: i18n.pmBotJobs }),
|
|
|
- h2({ class: 'pm-title' }, titleH),
|
|
|
- p(
|
|
|
- i18n.pmInhabitantWithId, ' ',
|
|
|
- linkAuthor(from), ' ',
|
|
|
- isSub ? i18n.pmHasSubscribedToYourJobOffer : (i18n.pmHasUnsubscribedFromYourJobOffer || 'has unsubscribed from your job offer'),
|
|
|
- ' ',
|
|
|
- href ? a({ class: 'job-link', href }, `"${jobTitle}"`) : `"${jobTitle}"`
|
|
|
- ),
|
|
|
- actions({ key, replyId: from })
|
|
|
- );
|
|
|
- }
|
|
|
+ function threadId(m) {
|
|
|
+ return canonicalSubject(m?.value?.content?.subject || '') + '||' + participantsKey(m)
|
|
|
+ }
|
|
|
|
|
|
- function ProjectFollowCard({ type, sentAt, from, toLinks, text, key }) {
|
|
|
- const isFollow = type === 'PROJECT_FOLLOWED';
|
|
|
- const icon = isFollow ? '🔔' : '🔕';
|
|
|
- const titleH = isFollow
|
|
|
- ? (i18n.inboxProjectFollowedTitle || 'New follower of your project')
|
|
|
- : (i18n.inboxProjectUnfollowedTitle || 'Unfollowed your project');
|
|
|
- const projectTitle = quoted(text) || 'project';
|
|
|
- const projectId = pickLink(text, 'project');
|
|
|
- const href = projectId ? hrefFor.project(projectId) : null;
|
|
|
- return div(
|
|
|
- clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification`),
|
|
|
- header({ sentAt, from, toLinks, botIcon: icon, botLabel: i18n.pmBotProjects }),
|
|
|
- h2({ class: 'pm-title' }, titleH),
|
|
|
- p(
|
|
|
- i18n.pmInhabitantWithId, ' ',
|
|
|
- a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}` }, from),
|
|
|
- ' ',
|
|
|
- isFollow ? (i18n.pmHasFollowedYourProject || 'has followed your project') : (i18n.pmHasUnfollowedYourProject || 'has unfollowed your project'),
|
|
|
- ' ',
|
|
|
- href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
|
|
|
- ),
|
|
|
- actions({ key, replyId: from })
|
|
|
- );
|
|
|
- }
|
|
|
+ function threadLevel(s) {
|
|
|
+ const m = (s || '').match(/RE:/gi)
|
|
|
+ return m ? Math.min(m.length, 8) : 0
|
|
|
+ }
|
|
|
|
|
|
- function MarketSoldCard({ sentAt, from, toLinks, subject, text, key }) {
|
|
|
- const itemTitle = quoted(subject) || quoted(text) || 'item';
|
|
|
- const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from;
|
|
|
- const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || '';
|
|
|
- const marketId = pickLink(text, 'market');
|
|
|
- const href = marketId ? hrefFor.market(marketId) : null;
|
|
|
- return div(
|
|
|
- clickableCardProps(href, 'market-sold-notification'),
|
|
|
- header({ sentAt, from, toLinks, botIcon: '💰', botLabel: i18n.pmBotMarket }),
|
|
|
- h2({ class: 'pm-title' }, i18n.inboxMarketItemSoldTitle),
|
|
|
- p(
|
|
|
- i18n.pmYourItem, ' ',
|
|
|
- href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
|
|
|
- ' ',
|
|
|
- i18n.pmHasBeenSoldTo, ' ',
|
|
|
- linkAuthor(buyerId),
|
|
|
- price ? ` ${i18n.pmFor} $${price}.` : '.'
|
|
|
- ),
|
|
|
- actions({ key, replyId: buyerId })
|
|
|
- );
|
|
|
- }
|
|
|
+ function quoted(str) {
|
|
|
+ const m = str.match(/"([^"]+)"/)
|
|
|
+ return m ? m[1] : ''
|
|
|
+ }
|
|
|
|
|
|
- function ProjectPledgeCard({ sentAt, from, toLinks, content, text, key }) {
|
|
|
- const amount = content.meta?.amount ?? (text.match(/pledged\s+([\d.]+)/)?.[1] || '0');
|
|
|
- const projectTitle = content.meta?.projectTitle ?? (text.match(/project\s+"([^"]+)"/)?.[1] || 'project');
|
|
|
- const projectId = content.meta?.projectId ?? pickLink(text, 'project');
|
|
|
- const href = projectId ? hrefFor.project(projectId) : null;
|
|
|
- return div(
|
|
|
- clickableCardProps(href, 'project-pledge-notification'),
|
|
|
- header({ sentAt, from, toLinks, botIcon: '💚', botLabel: i18n.pmBotProjects }),
|
|
|
- h2({ class: 'pm-title' }, i18n.inboxProjectPledgedTitle),
|
|
|
- p(
|
|
|
- i18n.pmInhabitantWithId, ' ',
|
|
|
- linkAuthor(from), ' ',
|
|
|
- i18n.pmHasPledged, ' ',
|
|
|
- chip(`${amount} ECO`), ' ',
|
|
|
- i18n.pmToYourProject, ' ',
|
|
|
- href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
|
|
|
- ),
|
|
|
- actions({ key, replyId: from })
|
|
|
- );
|
|
|
+ function pickLink(str, kind) {
|
|
|
+ if (kind === 'job') {
|
|
|
+ const m = str.match(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/)
|
|
|
+ return m ? m[1] : ''
|
|
|
}
|
|
|
-
|
|
|
- function clickableLinks(str) {
|
|
|
- return str
|
|
|
- .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g,
|
|
|
- (match, id) => `<a class="user-link" href="/author/${encodeURIComponent(id)}">${match}</a>`
|
|
|
- )
|
|
|
- .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g,
|
|
|
- (match, id) => `<a class="job-link" href="${hrefFor.job(id)}">${match}</a>`
|
|
|
- )
|
|
|
- .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g,
|
|
|
- (match, id) => `<a class="project-link" href="${hrefFor.project(id)}">${match}</a>`
|
|
|
- )
|
|
|
- .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g,
|
|
|
- (match, id) => `<a class="market-link" href="${hrefFor.market(id)}">${match}</a>`
|
|
|
- );
|
|
|
+ if (kind === 'project') {
|
|
|
+ const m = str.match(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/)
|
|
|
+ return m ? m[1] : ''
|
|
|
+ }
|
|
|
+ if (kind === 'market') {
|
|
|
+ const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/)
|
|
|
+ return m ? m[1] : ''
|
|
|
}
|
|
|
+ return ''
|
|
|
+ }
|
|
|
|
|
|
- return template(
|
|
|
- i18n.private,
|
|
|
- section(
|
|
|
- div({ class: 'tags-header' },
|
|
|
- h2(i18n.private),
|
|
|
- p(i18n.privateDescription)
|
|
|
- ),
|
|
|
- div({ class: 'filters' },
|
|
|
- form({ method: 'GET', action: '/inbox' }, [
|
|
|
- button({
|
|
|
- type: 'submit',
|
|
|
- name: 'filter',
|
|
|
- value: 'inbox',
|
|
|
- class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn'
|
|
|
- }, i18n.privateInbox),
|
|
|
- button({
|
|
|
- type: 'submit',
|
|
|
- name: 'filter',
|
|
|
- value: 'sent',
|
|
|
- class: filter === 'sent' ? 'filter-btn active' : 'filter-btn'
|
|
|
- }, i18n.privateSent),
|
|
|
- button({
|
|
|
- type: 'submit',
|
|
|
- name: 'filter',
|
|
|
- value: 'create',
|
|
|
- class: 'create-button',
|
|
|
- formaction: '/pm',
|
|
|
- formmethod: 'GET'
|
|
|
- }, i18n.pmCreateButton)
|
|
|
- ])
|
|
|
- ),
|
|
|
- div({ class: 'message-list' },
|
|
|
- filtered.length
|
|
|
- ? filtered.map(msg => {
|
|
|
- const content = msg?.value?.content;
|
|
|
- const author = msg?.value?.author;
|
|
|
- if (!content || !author) return div({ class: 'pm-card malformed' }, i18n.pmInvalidMessage);
|
|
|
- const subjectRaw = content.subject || '';
|
|
|
- const subject = subjectRaw.toUpperCase();
|
|
|
- const text = content.text || '';
|
|
|
- const sentAt = new Date(content.sentAt || msg.timestamp);
|
|
|
- const from = content.from;
|
|
|
- const toLinks = (content.to || []).map(addr => linkAuthor(addr));
|
|
|
-
|
|
|
- if (subject === 'JOB_SUBSCRIBED' || subject === 'JOB_UNSUBSCRIBED') {
|
|
|
- return JobCard({ type: subject, sentAt, from, toLinks, text, key: msg.key });
|
|
|
- }
|
|
|
- if (subject === 'PROJECT_FOLLOWED' || subject === 'PROJECT_UNFOLLOWED') {
|
|
|
- return ProjectFollowCard({ type: subject, sentAt, from, toLinks, text, key: msg.key });
|
|
|
- }
|
|
|
- if (subject === 'MARKET_SOLD') {
|
|
|
- return MarketSoldCard({ sentAt, from, toLinks, subject: subjectRaw, text, key: msg.key });
|
|
|
- }
|
|
|
- if (subject === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') {
|
|
|
- return ProjectPledgeCard({ sentAt, from, toLinks, content, text, key: msg.key });
|
|
|
- }
|
|
|
-
|
|
|
- const jobTxt = text.match(/has subscribed to your job offer "([^"]+)"/);
|
|
|
- const jobIdLegacy = pickLink(text, 'job');
|
|
|
- if (jobTxt && jobIdLegacy) return JobCard({ type: 'JOB_SUBSCRIBED', sentAt, from, toLinks, text, key: msg.key });
|
|
|
-
|
|
|
- const projTxt = text.match(/has created a project "([^"]+)"/);
|
|
|
- const projIdLegacy = pickLink(text, 'project');
|
|
|
- if (projTxt && projIdLegacy) return ProjectCreatedCard({ sentAt, from, toLinks, text, key: msg.key });
|
|
|
-
|
|
|
- const saleTxt = subjectRaw.match(/item "([^"]+)" has been sold/) || text.match(/item "([^"]+)" has been sold/);
|
|
|
- const marketIdLegacy = pickLink(text, 'market');
|
|
|
- if (saleTxt && marketIdLegacy) return MarketSoldCard({ sentAt, from, toLinks, subject: subjectRaw, text, key: msg.key });
|
|
|
-
|
|
|
- return div(
|
|
|
- { class: 'pm-card normal-pm' },
|
|
|
- header({ sentAt, from, toLinks }),
|
|
|
- h2(content.subject || i18n.pmNoSubject),
|
|
|
- p({ class: 'message-text' }, ...renderUrl(clickableLinks(text))),
|
|
|
- actions({ key: msg.key, replyId: from })
|
|
|
- );
|
|
|
- })
|
|
|
- : p({ class: 'empty' }, i18n.noPrivateMessages)
|
|
|
- )
|
|
|
- )
|
|
|
- );
|
|
|
-};
|
|
|
+ function clickableLinks(str) {
|
|
|
+ return str
|
|
|
+ .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `<a class="user-link" href="/author/${encodeURIComponent(id)}">${match}</a>`)
|
|
|
+ .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="job-link" href="${hrefFor.job(id)}">${match}</a>`)
|
|
|
+ .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="project-link" href="${hrefFor.project(id)}">${match}</a>`)
|
|
|
+ .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="market-link" href="${hrefFor.market(id)}">${match}</a>`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const threads = {}
|
|
|
+ for (const m of messages) {
|
|
|
+ const tid = threadId(m)
|
|
|
+ if (!threads[tid]) threads[tid] = []
|
|
|
+ threads[tid].push(m)
|
|
|
+ }
|
|
|
+
|
|
|
+ const inboxSet = new Set()
|
|
|
+ for (const arr of Object.values(threads)) {
|
|
|
+ const hasInbound = arr.some(isToUser)
|
|
|
+ if (hasInbound) for (const m of arr) inboxSet.add(m)
|
|
|
+ }
|
|
|
+
|
|
|
+ const data =
|
|
|
+ filter === 'sent' ? messages.filter(isSent) :
|
|
|
+ filter === 'inbox' ? Array.from(inboxSet) :
|
|
|
+ messages
|
|
|
+
|
|
|
+ const inboxCount = Array.from(inboxSet).length
|
|
|
+ const sentCount = messages.filter(isSent).length
|
|
|
+
|
|
|
+ const sorted = [...data].sort((a, b) => {
|
|
|
+ const ta = threadId(a)
|
|
|
+ const tb = threadId(b)
|
|
|
+ if (ta < tb) return -1
|
|
|
+ if (ta > tb) return 1
|
|
|
+ const sa = new Date(a?.value?.content?.sentAt || a.timestamp || 0).getTime()
|
|
|
+ const sb = new Date(b?.value?.content?.sentAt || b.timestamp || 0).getTime()
|
|
|
+ return sa - sb
|
|
|
+ })
|
|
|
+
|
|
|
+ function JobCard({ type, sentAt, from, toLinks, text, key }) {
|
|
|
+ const isSub = type === 'JOB_SUBSCRIBED'
|
|
|
+ const icon = isSub ? '🟡' : '🟠'
|
|
|
+ const titleH = isSub ? (i18n.inboxJobSubscribedTitle || 'New subscription to your job offer') : (i18n.inboxJobUnsubscribedTitle || 'Unsubscription from your job offer')
|
|
|
+ const jobTitle = quoted(text) || 'job'
|
|
|
+ const jobId = pickLink(text, 'job')
|
|
|
+ const href = jobId ? hrefFor.job(jobId) : null
|
|
|
+ return div(
|
|
|
+ clickableCardProps(href, `job-notification thread-level-0`),
|
|
|
+ headerLine({ sentAt, from, toLinks, textLen: text.length }),
|
|
|
+ h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`),
|
|
|
+ p(
|
|
|
+ i18n.pmInhabitantWithId, ' ',
|
|
|
+ linkAuthor(from), ' ',
|
|
|
+ isSub ? i18n.pmHasSubscribedToYourJobOffer : (i18n.pmHasUnsubscribedFromYourJobOffer || 'has unsubscribed from your job offer'),
|
|
|
+ ' ',
|
|
|
+ href ? a({ class: 'job-link', href }, `"${jobTitle}"`) : `"${jobTitle}"`
|
|
|
+ ),
|
|
|
+ actions({ key, replyId: from, subjectRaw: jobTitle, text })
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ function ProjectFollowCard({ type, sentAt, from, toLinks, text, key }) {
|
|
|
+ const isFollow = type === 'PROJECT_FOLLOWED'
|
|
|
+ const icon = isFollow ? '🔔' : '🔕'
|
|
|
+ const titleH = isFollow
|
|
|
+ ? (i18n.inboxProjectFollowedTitle || 'New follower of your project')
|
|
|
+ : (i18n.inboxProjectUnfollowedTitle || 'Unfollowed your project')
|
|
|
+ const projectTitle = quoted(text) || 'project'
|
|
|
+ const projectId = pickLink(text, 'project')
|
|
|
+ const href = projectId ? hrefFor.project(projectId) : null
|
|
|
+ return div(
|
|
|
+ clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`),
|
|
|
+ headerLine({ sentAt, from, toLinks, textLen: text.length }),
|
|
|
+ h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`),
|
|
|
+ p(
|
|
|
+ i18n.pmInhabitantWithId, ' ',
|
|
|
+ a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}` }, from),
|
|
|
+ ' ',
|
|
|
+ isFollow ? (i18n.pmHasFollowedYourProject || 'has followed your project') : (i18n.pmHasUnfollowedYourProject || 'has unfollowed your project'),
|
|
|
+ ' ',
|
|
|
+ href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
|
|
|
+ ),
|
|
|
+ actions({ key, replyId: from, subjectRaw: projectTitle, text })
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ function MarketSoldCard({ sentAt, from, toLinks, subject, text, key }) {
|
|
|
+ const itemTitle = quoted(subject) || quoted(text) || 'item'
|
|
|
+ const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from
|
|
|
+ const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || ''
|
|
|
+ const marketId = pickLink(text, 'market')
|
|
|
+ const href = marketId ? hrefFor.market(marketId) : null
|
|
|
+ return div(
|
|
|
+ clickableCardProps(href, 'market-sold-notification thread-level-0'),
|
|
|
+ headerLine({ sentAt, from, toLinks, textLen: text.length }),
|
|
|
+ h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`),
|
|
|
+ p(
|
|
|
+ i18n.pmYourItem, ' ',
|
|
|
+ href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
|
|
|
+ ' ',
|
|
|
+ i18n.pmHasBeenSoldTo, ' ',
|
|
|
+ linkAuthor(buyerId),
|
|
|
+ price ? ` ${i18n.pmFor} $${price}.` : '.'
|
|
|
+ ),
|
|
|
+ actions({ key, replyId: buyerId, subjectRaw: itemTitle, text })
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ function ProjectPledgeCard({ sentAt, from, toLinks, content, text, key }) {
|
|
|
+ const amount = content.meta?.amount ?? (text.match(/pledged\s+([\d.]+)/)?.[1] || '0')
|
|
|
+ const projectTitle = content.meta?.projectTitle ?? (text.match(/project\s+"([^"]+)"/)?.[1] || 'project')
|
|
|
+ const projectId = content.meta?.projectId ?? pickLink(text, 'project')
|
|
|
+ const href = projectId ? hrefFor.project(projectId) : null
|
|
|
+ return div(
|
|
|
+ clickableCardProps(href, 'project-pledge-notification thread-level-0'),
|
|
|
+ headerLine({ sentAt, from, toLinks, textLen: text.length }),
|
|
|
+ h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`),
|
|
|
+ p(
|
|
|
+ i18n.pmInhabitantWithId, ' ',
|
|
|
+ linkAuthor(from), ' ',
|
|
|
+ i18n.pmHasPledged, ' ',
|
|
|
+ chip(`${amount} ECO`), ' ',
|
|
|
+ i18n.pmToYourProject, ' ',
|
|
|
+ href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
|
|
|
+ ),
|
|
|
+ actions({ key, replyId: from, subjectRaw: projectTitle, text })
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return template(
|
|
|
+ i18n.private,
|
|
|
+ section(
|
|
|
+ div({ class: 'tags-header' },
|
|
|
+ h2(i18n.private),
|
|
|
+ p(i18n.privateDescription)
|
|
|
+ ),
|
|
|
+ div({ class: 'filters' },
|
|
|
+ form({ method: 'GET', action: '/inbox' }, [
|
|
|
+ button({
|
|
|
+ type: 'submit',
|
|
|
+ name: 'filter',
|
|
|
+ value: 'inbox',
|
|
|
+ class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn'
|
|
|
+ }, `${i18n.privateInbox} (${inboxCount})`),
|
|
|
+ button({
|
|
|
+ type: 'submit',
|
|
|
+ name: 'filter',
|
|
|
+ value: 'sent',
|
|
|
+ class: filter === 'sent' ? 'filter-btn active' : 'filter-btn'
|
|
|
+ }, `${i18n.privateSent} (${sentCount})`),
|
|
|
+ button({
|
|
|
+ type: 'submit',
|
|
|
+ name: 'filter',
|
|
|
+ value: 'create',
|
|
|
+ class: 'create-button',
|
|
|
+ formaction: '/pm',
|
|
|
+ formmethod: 'GET'
|
|
|
+ }, i18n.pmCreateButton)
|
|
|
+ ])
|
|
|
+ ),
|
|
|
+ div({ class: 'message-list' },
|
|
|
+ sorted.length
|
|
|
+ ? sorted.map(msg => {
|
|
|
+ const content = msg.value.content
|
|
|
+ const author = msg.value.author
|
|
|
+ const subjectRaw = content.subject || ''
|
|
|
+ const subjectU = subjectRaw.toUpperCase()
|
|
|
+ const text = content.text || ''
|
|
|
+ const sentAt = new Date(content.sentAt || msg.timestamp)
|
|
|
+ const fromResolved = content.from || author
|
|
|
+ const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : []
|
|
|
+ const level = threadLevel(subjectRaw)
|
|
|
+
|
|
|
+ if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') {
|
|
|
+ return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
|
|
|
+ }
|
|
|
+ if (subjectU === 'PROJECT_FOLLOWED' || subjectU === 'PROJECT_UNFOLLOWED') {
|
|
|
+ return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
|
|
|
+ }
|
|
|
+ if (subjectU === 'MARKET_SOLD') {
|
|
|
+ return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key })
|
|
|
+ }
|
|
|
+ if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') {
|
|
|
+ return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key })
|
|
|
+ }
|
|
|
+
|
|
|
+ return div(
|
|
|
+ { class: `pm-card normal-pm thread-level-${level}` },
|
|
|
+ headerLine({ sentAt, from: fromResolved, toLinks, textLen: text.length }),
|
|
|
+ h2(subjectRaw || i18n.pmNoSubject),
|
|
|
+ p({ class: 'message-text' }, ...renderUrl(clickableLinks(text))),
|
|
|
+ actions({ key: msg.key, replyId: fromResolved, subjectRaw, text })
|
|
|
+ )
|
|
|
+ })
|
|
|
+ : p({ class: 'empty' }, i18n.noPrivateMessages)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+}
|
|
|
|
|
|
exports.publishCustomView = async () => {
|
|
|
const action = "/publish/custom";
|