Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.
Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).
Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
441 lines
20 KiB
JavaScript
441 lines
20 KiB
JavaScript
const {
|
|
div, a, span, form, button, section, p, ul, li,
|
|
input, label, br, select, option, h2, textarea, details, summary
|
|
} = require("../server/node_modules/hyperaxe");
|
|
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
|
const moment = require("../server/node_modules/moment");
|
|
const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderStateChip, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require('./main_views');
|
|
const { renderEncryptedChip: renderForumEncryptedChip, renderTribeOriginChip } = require('./clearnet_view');
|
|
const { config } = require('../server/SSB_server.js');
|
|
const { renderStyledText } = require('../backend/renderStyledText');
|
|
const { renderStyledHtml } = require('../backend/renderStyledText');
|
|
const { sanitizeHtml } = require('../backend/sanitizeHtml');
|
|
const { TEXT_CAP } = require('../backend/long_text');
|
|
|
|
const userId = config.keys.id;
|
|
|
|
exports.renderForumInvitePage = (code) => {
|
|
const pageContent = div({ class: 'invite-page' },
|
|
h2(i18n.tribeInviteCodeText, code),
|
|
form({ method: 'GET', action: '/forum' },
|
|
button({ type: 'submit', class: 'filter-btn' }, i18n.walletBack)
|
|
)
|
|
);
|
|
return template(i18n.invitesForumsTitle || 'Forums', section(pageContent));
|
|
};
|
|
|
|
const BASE_FILTERS = ['hot','all','recent','mine','top'];
|
|
const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.'];
|
|
const CAT_BLOCK2 = ['POLITICS','TECH','SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS'];
|
|
const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM'];
|
|
const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3];
|
|
|
|
const catKey = (c) => 'forumCat' + String(c || '').replace(/\./g,'').replace(/[\s-]/g,'').toUpperCase();
|
|
const catLabel = (c) => i18n[catKey(c)] || c;
|
|
const tribeCatLabel = (c) => i18n['tribeForumCat' + String(c || '').charAt(0) + String(c || '').slice(1).toLowerCase()] || c;
|
|
|
|
const Z = 1.96;
|
|
function wilsonScore(pos, neg) {
|
|
const n = (pos||0)+(neg||0);
|
|
if (n === 0) return 0;
|
|
const phat = pos / n, z2 = Z * Z;
|
|
return (phat + z2/(2*n) - Z*Math.sqrt((phat*(1-phat)+z2/(4*n))/n)) / (1+z2/n);
|
|
}
|
|
|
|
function getFilteredForums(filter, forums) {
|
|
const now = Date.now();
|
|
if (filter === 'mine') return forums.filter(f => f.author === userId);
|
|
if (filter === 'recent') return forums.filter(f => new Date(f.createdAt).getTime() >= now - 86400000);
|
|
if (filter === 'top') return forums.filter(f => !f.tribeOrigin).sort((a,b) => b.score - a.score);
|
|
if (filter === 'hot') return forums
|
|
.filter(f => !f.tribeOrigin && new Date(f.createdAt).getTime() >= now - 86400000)
|
|
.sort((a,b) => b.score - a.score);
|
|
if (ALL_CATS.includes(filter))
|
|
return forums.filter(f => f.category === filter);
|
|
return forums;
|
|
}
|
|
|
|
const generateFilterButtons = (filters, currentFilter, action, i18nMap = {}) =>
|
|
div({ class: 'filter-group' },
|
|
filters.map(mode =>
|
|
form({ method: 'GET', action },
|
|
input({ type: 'hidden', name: 'filter', value: mode }),
|
|
button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
|
|
String(i18nMap[mode] || mode).toUpperCase()
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
const renderCreateForumButton = () =>
|
|
div({ class: 'forum-create-col' },
|
|
form({ method: 'GET', action: '/forum' },
|
|
button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button' },
|
|
i18n.forumCreateButton
|
|
)
|
|
)
|
|
);
|
|
|
|
const renderVotes = (target, score, forumId) =>
|
|
div({ class: 'forum-score-box' },
|
|
form({ method: 'POST', action: `/forum/${encodeURIComponent(forumId)}/vote`, class: 'forum-score-form' },
|
|
button({ name: 'value', value: 1, class: 'score-btn' }, '▲'),
|
|
div({ class: 'score-total' }, String(score || 0)),
|
|
button({ name: 'value', value: -1, class: 'score-btn' }, '▼'),
|
|
input({ type: 'hidden', name: 'target', value: target }),
|
|
input({ type: 'hidden', name: 'forumId', value: forumId })
|
|
)
|
|
);
|
|
|
|
const renderForumForm = (draft = null) =>
|
|
div({ class: 'forum-form' },
|
|
form({ action: '/forum/create', method: 'POST', enctype: 'multipart/form-data' },
|
|
label(i18n.forumCategoryLabel), br(),
|
|
select({ name: 'category', required: true },
|
|
ALL_CATS.map(cat => option({ value: cat, ...(draft && draft.category === cat ? { selected: true } : {}) }, catLabel(cat)))
|
|
), br(), br(),
|
|
label(i18n.eventPrivacyLabel), br(),
|
|
select({ name: 'isPublic', id: 'isPublic' },
|
|
option({ value: 'public', ...(draft && draft.isPrivate ? {} : { selected: true }) }, i18n.eventPublic),
|
|
option({ value: 'private', ...(draft && draft.isPrivate ? { selected: true } : {}) }, i18n.eventPrivate)
|
|
), br(), br(),
|
|
label(i18n.forumTitleLabel), br(),
|
|
input({
|
|
type: 'text',
|
|
name: 'title', maxlength: '100',
|
|
required: true,
|
|
placeholder: i18n.forumTitlePlaceholder,
|
|
value: draft ? String(draft.title || '') : ''
|
|
}), br(), br(),
|
|
label(i18n.forumMessageLabel), br(),
|
|
textarea({ maxlength: String(TEXT_CAP),
|
|
name: 'text',
|
|
required: true,
|
|
rows: 8,
|
|
placeholder: i18n.forumMessagePlaceholder
|
|
}, draft ? String(draft.text || '') : ''), br(), br(),
|
|
label(i18n.uploadMedia), br(),
|
|
input({ type: 'file', name: 'blob' }), br(), br(),
|
|
button({ type: 'submit' }, i18n.forumCreateButton)
|
|
)
|
|
);
|
|
|
|
const bySupport = (a, b) =>
|
|
wilsonScore(b.positiveVotes, b.negativeVotes) - wilsonScore(a.positiveVotes, a.negativeVotes);
|
|
|
|
const bestReplyKey = (nodes) => {
|
|
const flat = [];
|
|
const walk = (list) => { for (const m of Array.isArray(list) ? list : []) { if (m && m.text && String(m.text).trim()) flat.push(m); walk(m && m.children); } };
|
|
walk(nodes);
|
|
if (!flat.length) return null;
|
|
const sorted = flat.slice().sort(bySupport);
|
|
const top = sorted[0];
|
|
if (wilsonScore(top.positiveVotes, top.negativeVotes) > 0) return top.key;
|
|
const first = (Array.isArray(nodes) ? nodes : []).filter(m => m && m.text && String(m.text).trim()).sort(bySupport)[0];
|
|
return first ? first.key : null;
|
|
};
|
|
|
|
const renderThread = (nodes, level = 0, forumId, bestKey = null) => {
|
|
if (!Array.isArray(nodes)) return [];
|
|
return [...nodes]
|
|
.filter(m => (m && m.text && String(m.text).trim()) || (m && Array.isArray(m.children) && m.children.length))
|
|
.sort(bySupport)
|
|
.flatMap((m) => {
|
|
const classList = [
|
|
'forum-comment',
|
|
`level-${level}`,
|
|
bestKey && m.key === bestKey ? 'highlighted-reply' : ''
|
|
].filter(Boolean).join(' ');
|
|
|
|
const commentBox = div(
|
|
{ class: classList, id: `msg-${String(m.key || '').replace(/[^a-zA-Z0-9]/g, '')}` },
|
|
div({ class: 'comment-header' },
|
|
span({ class: 'date-link' },
|
|
`${moment(m.timestamp).format('YYYY/MM/DD HH:mm')}`),
|
|
userLink(m.author),
|
|
div({ class: 'comment-votes' },
|
|
span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`),
|
|
span({ class: 'votes-count votes-count-negative' },
|
|
`▼: ${m.negativeVotes || 0}`)
|
|
)
|
|
),
|
|
div({ class: 'comment-body-row' },
|
|
div({ class: 'comment-vote-col' },
|
|
renderVotes(m.key, m.score, forumId)
|
|
),
|
|
div({ class: 'comment-text-col' },
|
|
div(
|
|
...(m.text || '').split('\n')
|
|
.map(l => l.trim())
|
|
.filter(l => l)
|
|
.map(l => p(...renderStyledText(l, { zoomImages: true })))
|
|
)
|
|
)
|
|
),
|
|
details({ class: 'new-reply forum-inline-reply', ...(draftFor(m.key) ? { open: true } : {}) },
|
|
summary({ class: 'forum-inline-reply-toggle' }, i18n.tribeForumReply),
|
|
form({
|
|
method: 'POST',
|
|
action: `/forum/${encodeURIComponent(forumId)}/message`,
|
|
class: 'comment-form',
|
|
enctype: 'multipart/form-data'
|
|
},
|
|
input({ type: 'hidden', name: 'parentId', value: m.key }),
|
|
textarea({ maxlength: String(TEXT_CAP),
|
|
name: 'message',
|
|
rows: 4,
|
|
required: true,
|
|
placeholder: i18n.forumMessagePlaceholder,
|
|
class: 'comment-textarea'
|
|
}, draftFor(m.key)),
|
|
div({ class: 'comment-file-upload' }, input({ type: 'file', name: 'blob' })),
|
|
button({ type: 'submit', class: 'forum-send-btn' }, i18n.tribeForumReply)
|
|
)
|
|
)
|
|
);
|
|
|
|
return [ commentBox, ...renderThread(m.children || [], level + 1, forumId, bestKey) ];
|
|
});
|
|
};
|
|
|
|
const forumHref = (f) => `/forum/${encodeURIComponent(f.key)}`;
|
|
|
|
const forumCounts = (f) => span({ class: 'card-label activity-update-counts mailing-counts' },
|
|
`👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)} · ▲ ${f.positiveVotes || 0} · ▼ ${f.negativeVotes || 0}`);
|
|
|
|
const renderForumList = (forums, currentFilter, spreadMap = new Map()) => {
|
|
const visibleForums = (Array.isArray(forums) ? forums : []).filter(f =>
|
|
(f && f.title && String(f.title).trim()) || (f && f.text && String(f.text).trim())
|
|
)
|
|
if (!visibleForums.length) return div({ class: 'tribe-grid' }, p(i18n.noForums));
|
|
return ul({ class: 'mailing-archive' },
|
|
...paged(visibleForums).map(f => {
|
|
if (f.tribeOrigin) {
|
|
return li({ class: 'mailing-archive-item' },
|
|
div({ class: 'emergency-update-head mailing-archive-head' },
|
|
div({ class: 'mailing-archive-meta' },
|
|
a({ href: f.tribeOrigin.href, class: 'user-link' }, f.title || '—'),
|
|
ALL_CATS.includes(f.category)
|
|
? a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`)
|
|
: (f.category ? span({ class: 'forum-category' }, `[${tribeCatLabel(f.category)}]`) : null),
|
|
span({ class: 'card-label activity-update-counts mailing-counts' }, `👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)}`),
|
|
renderLifespanChip(f.lifetime, i18n),
|
|
renderTribeOriginChip(f.tribeOrigin)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
return li({ class: 'mailing-archive-item' },
|
|
div({ class: 'emergency-update-head mailing-archive-head' },
|
|
div({ class: 'mailing-archive-meta' },
|
|
a({ href: forumHref(f), class: 'user-link' }, f.title || '—'),
|
|
a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`),
|
|
forumCounts(f),
|
|
f.isPrivate ? renderPrivacyChip(true, i18n) : null,
|
|
f.isPrivate ? renderForumEncryptedChip(i18n) : null,
|
|
renderLifespanChip(f.lifetime, i18n),
|
|
f.subscriptionIn === true
|
|
? renderStateChip('mutuals', '✉', i18n.subscriptionOn)
|
|
: (f.subscriptionIn === false ? renderStateChip('closed', '✉', i18n.subscriptionOff) : null)
|
|
),
|
|
renderContentActions(f.key, forumHref(f), { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title, deleteAction: f.author === userId ? `/forum/delete/${encodeURIComponent(f.key)}` : null })
|
|
)
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
exports.forumView = async (forums, currentFilter, params = {}) => {
|
|
const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
|
|
const emptyModForum = moduleIsEmpty(getFilteredForums(currentFilter || 'all', forums), currentFilter || 'all', 'all', params.q);
|
|
const censusForums = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(forums) ? forums : []);
|
|
const presentCats = new Set(censusForums.map(f => f && f.category).filter(Boolean));
|
|
const baseChipVisible = (mode) => {
|
|
if (mode === currentFilter || mode === 'all') return true;
|
|
if (mode === 'mine') return censusForums.some(f => String(f.author) === String(userId));
|
|
if (mode === 'recent') return censusForums.length > 0;
|
|
return censusForums.length > 0;
|
|
};
|
|
return template(i18n.forumTitle,
|
|
section(
|
|
div({ class: 'tags-header module-header-line' },
|
|
h2(i18n.forumTitle),
|
|
p(i18n.forumDescription)
|
|
),
|
|
div({ class: 'mode-buttons-row' },
|
|
...(emptyModForum ? [] : [
|
|
generateFilterButtons(BASE_FILTERS.filter(baseChipVisible), currentFilter, '/forum', {
|
|
hot: i18n.forumFilterHot,
|
|
all: i18n.forumFilterAll,
|
|
mine: i18n.forumFilterMine,
|
|
recent: i18n.forumFilterRecent,
|
|
top: i18n.forumFilterTop
|
|
}),
|
|
generateFilterButtons(CAT_BLOCK1.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP),
|
|
generateFilterButtons(CAT_BLOCK2.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP),
|
|
generateFilterButtons(CAT_BLOCK3.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP)
|
|
]),
|
|
renderCreateForumButton()
|
|
),
|
|
currentFilter === 'create'
|
|
? null
|
|
: emptyModForum ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' },
|
|
renderModuleStats(getFilteredForums(currentFilter || 'all', forums).length),
|
|
form({ method: 'GET', action: '/forum', class: 'filter-box' },
|
|
input({ type: 'hidden', name: 'filter', value: currentFilter || 'all' }),
|
|
input({ type: 'text', name: 'q', value: params.q || '', placeholder: i18n.forumSearchPlaceholder, class: 'filter-box__input' }),
|
|
div({ class: 'filter-box__controls' },
|
|
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
|
)
|
|
)
|
|
),
|
|
currentFilter === 'create'
|
|
? renderForumForm(params.draft || null)
|
|
: renderForumList(
|
|
getFilteredForums(currentFilter || 'all', forums),
|
|
currentFilter,
|
|
params.spreadMap
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
let replyDraft = null;
|
|
const draftFor = (parentId) => replyDraft && String(replyDraft.parentId || '') === String(parentId || '') ? String(replyDraft.message || '') : '';
|
|
|
|
exports.singleForumView = async (forum, messagesData, currentFilter, replyId = null, params = {}) => {
|
|
replyDraft = params.draft || null;
|
|
const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
|
|
const isForumOwner = String(forum.author) === String(userId);
|
|
const sharesForum = isForumOwner || (forum.participants || []).includes(userId);
|
|
const forumSubIn = isForumOwner || (forum.subscription && forum.subscription.subscribed === true);
|
|
return template(forum.title,
|
|
section(
|
|
div({ class: 'tags-header module-header-line' },
|
|
h2(i18n.forumTitle),
|
|
p(i18n.forumDescription)
|
|
),
|
|
div({ class: 'mode-buttons-row' },
|
|
generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', {
|
|
hot: i18n.forumFilterHot,
|
|
all: i18n.forumFilterAll,
|
|
mine: i18n.forumFilterMine,
|
|
recent: i18n.forumFilterRecent,
|
|
top: i18n.forumFilterTop
|
|
}),
|
|
generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum', CAT_I18N_MAP_UP),
|
|
generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum', CAT_I18N_MAP_UP),
|
|
generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP),
|
|
renderCreateForumButton()
|
|
)
|
|
),
|
|
div({ class: 'forum-thread-container' },
|
|
div({ class: 'forum-card forum-thread-header' },
|
|
div({ class: 'root-vote-col' }, renderVotes(
|
|
forum.key,
|
|
messagesData.totalScore,
|
|
forum.key
|
|
)),
|
|
div({ class: 'forum-main-col' },
|
|
div({ class: 'card-header activity-card-header' },
|
|
renderContentActions(forum.key, forumHref(forum), {
|
|
author: forum.author,
|
|
favKind: 'forum',
|
|
isFavorite: forum.isFavorite,
|
|
spread: params.spreads || null,
|
|
reportTitle: forum.title,
|
|
deleteAction: isForumOwner ? `/forum/delete/${encodeURIComponent(forum.key)}` : null
|
|
})
|
|
),
|
|
div({ class: 'forum-header-row' },
|
|
a({
|
|
class: 'forum-category',
|
|
href: `/forum?filter=${encodeURIComponent(forum.category)}`
|
|
}, `[${catLabel(forum.category)}]`),
|
|
a({
|
|
class: 'forum-title',
|
|
href: `/forum/${encodeURIComponent(forum.key)}`
|
|
}, forum.title),
|
|
forum.isPrivate ? renderPrivacyChip(true, i18n) : null,
|
|
forum.isPrivate ? renderForumEncryptedChip(i18n) : null,
|
|
renderLifespanChip(forum.lifetime, i18n),
|
|
(forum.subscription && sharesForum)
|
|
? renderStateChip(forumSubIn ? 'mutuals' : 'closed', '✉', forumSubIn ? i18n.subscriptionOn : i18n.subscriptionOff)
|
|
: null,
|
|
(forum.isPrivate && forum.author === userId)
|
|
? form({ method: 'POST', action: `/forum/generate-invite/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' },
|
|
button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeGenerateInvite))
|
|
: null,
|
|
(forum.isPrivate && forum.author === userId && !forum.openInviteCode)
|
|
? form({ method: 'POST', action: `/forum/open-invite/create/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' },
|
|
button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeOpenInvitation))
|
|
: null,
|
|
(forum.isPrivate && forum.author === userId && forum.openInviteCode)
|
|
? div({ class: 'tribe-open-invite' },
|
|
span({ class: 'card-label' }, i18n.tribeInviteCodeText),
|
|
span({ class: 'tribe-open-invite-code' }, forum.openInviteCode),
|
|
renderInviteQrCard({ qrDataUrl: `/qr-invite-code/forum/${encodeURIComponent(forum.openInviteCode)}` }))
|
|
: null,
|
|
(forum.isPrivate && forum.author === userId && forum.openInviteCode)
|
|
? form({ method: 'POST', action: `/forum/open-invite/remove/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' },
|
|
button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeRemoveInvitation))
|
|
: null,
|
|
(forum.isPrivate && forum.author !== userId)
|
|
? a({ class: 'tribe-action-btn', href: '/invites?filter=others' }, i18n.tribeEnterInvite)
|
|
: null
|
|
),
|
|
(forum.subscription && sharesForum)
|
|
? renderSubscriptionBox({
|
|
target: forum.rootId || forum.key,
|
|
scope: 'forum',
|
|
subscribed: forum.subscription.subscribed === true,
|
|
count: forum.subscription.count,
|
|
isOwner: isForumOwner,
|
|
returnTo: `/forum/${encodeURIComponent(forum.key)}`
|
|
})
|
|
: null,
|
|
div({
|
|
class: 'forum-body',
|
|
[RAW_HTML]: sanitizeHtml(renderStyledHtml(forum.text || ''))
|
|
}),
|
|
div({ class: 'forum-meta' },
|
|
span({ class: 'forum-positive-votes' },
|
|
`▲: ${messagesData.positiveVotes}`),
|
|
span({ class: 'forum-negative-votes' },
|
|
`▼: ${messagesData.negativeVotes}`),
|
|
span({ class: 'forum-participants' },
|
|
`${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`),
|
|
span({ class: 'forum-messages' },
|
|
`${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`)
|
|
),
|
|
div({ class: 'forum-footer' },
|
|
span({ class: 'date-link' },
|
|
`${moment(forum.createdAt).format('YYYY/MM/DD HH:mm')}`),
|
|
userLink(forum.author)
|
|
)
|
|
)
|
|
),
|
|
div({ class: 'new-message-wrapper' },
|
|
form({
|
|
method: 'POST',
|
|
action: `/forum/${encodeURIComponent(forum.key)}/message`,
|
|
class: 'new-message-form',
|
|
enctype: 'multipart/form-data'
|
|
},
|
|
textarea({ maxlength: String(TEXT_CAP),
|
|
name: 'message',
|
|
rows: 8,
|
|
required: true,
|
|
placeholder: i18n.forumMessagePlaceholder,
|
|
class: 'new-message-textarea'
|
|
}, draftFor(null)), br(),
|
|
label(i18n.uploadMedia), br(),
|
|
input({ type: 'file', name: 'blob' }), br(),
|
|
button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton)
|
|
)
|
|
),
|
|
...renderThread(messagesData.messages, 0, forum.key, bestReplyKey(messagesData.messages))
|
|
)
|
|
);
|
|
};
|
|
|