Upgrade mobile UX to Oasis 0.9.0
Rebase onto upstream 0.9.0. Mobile topbar (logo->home, avatar->profile), hexagon category nav (hive) and bottom bar; remove the six top-bar buttons (inbox/pm/publish/search/graphos/peers) that cluttered the header. Theme OasisMobileUX carries topbar/hexagon/bottombar styles.
This commit is contained in:
parent
de36b38851
commit
8d21216362
69 changed files with 8974 additions and 820 deletions
|
|
@ -23,7 +23,7 @@ const getUserId = async () => {
|
|||
return userId;
|
||||
};
|
||||
|
||||
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, table, td, textarea, title, tr, ul, strong, script, video: videoHyperaxe, audio: audioHyperaxe } = 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, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");
|
||||
|
||||
const lodash = require("../server/node_modules/lodash");
|
||||
const markdown = require("./markdown");
|
||||
|
|
@ -112,67 +112,6 @@ const renderContentActions = (msgId, viewHref) => {
|
|||
};
|
||||
exports.renderContentActions = renderContentActions;
|
||||
|
||||
const emptyState = ({ icon, title, text, ctaHref, ctaLabel }) => div(
|
||||
{ class: "empty-state" },
|
||||
icon ? span({ class: "empty-state-icon" }, icon) : "",
|
||||
title ? p({ class: "empty-state-title" }, title) : "",
|
||||
text ? p({ class: "empty-state-text" }, text) : "",
|
||||
(ctaHref && ctaLabel) ? a({ class: "empty-state-cta filter-btn", href: ctaHref }, ctaLabel) : ""
|
||||
);
|
||||
exports.emptyState = emptyState;
|
||||
|
||||
exports.bottomBarPickerView = () => {
|
||||
const cfg = getConfig();
|
||||
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
|
||||
const MAX = 4;
|
||||
const pinnedKeys = cfg.bottomBarPins || [];
|
||||
const pinned = pinnedKeys
|
||||
.map(key => exports.PINNABLE_MODULES.find(m => m.key === key))
|
||||
.filter(Boolean);
|
||||
const full = pinned.length >= MAX;
|
||||
const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls },
|
||||
input({ type: 'hidden', name: 'action', value: action }),
|
||||
input({ type: 'hidden', name: 'key', value: key }),
|
||||
node
|
||||
);
|
||||
const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form',
|
||||
button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' },
|
||||
span({ class: 'bb-manage-ico' }, m.glyph),
|
||||
span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key),
|
||||
span({ class: 'bb-manage-x' }, '✕')
|
||||
)
|
||||
);
|
||||
const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form',
|
||||
button({ type: 'submit', class: 'bb-pick-btn' },
|
||||
span({ class: 'bb-pick-ico' }, m.glyph),
|
||||
span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key)
|
||||
)
|
||||
);
|
||||
const addable = exports.PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key));
|
||||
return template(
|
||||
i18n.bbManage || 'Edit bar',
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h2(i18n.bbPickTitle || 'Customize the bar'),
|
||||
p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.')
|
||||
),
|
||||
div({ class: 'bb-manage' },
|
||||
h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`),
|
||||
pinned.length
|
||||
? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow))
|
||||
: p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None')
|
||||
),
|
||||
div({ class: 'bb-manage-add' },
|
||||
h3(i18n.bbAdd || 'Add'),
|
||||
full
|
||||
? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached')
|
||||
: div({ class: 'bb-pick-grid' }, ...addable.map(addBtn))
|
||||
),
|
||||
a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done')
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.renderStateChip = renderStateChip;
|
||||
exports.renderOpenClosedChip = renderOpenClosedChip;
|
||||
exports.renderVisibilityChip = renderVisibilityChip;
|
||||
|
|
@ -575,6 +514,21 @@ const renderWalletLink = () => {
|
|||
return "";
|
||||
};
|
||||
|
||||
const renderDevLink = () => {
|
||||
const devMod = getConfig().modules.devMod === "on";
|
||||
if (devMod) {
|
||||
return [
|
||||
navLink({
|
||||
href: "/dev",
|
||||
emoji: "⌘",
|
||||
text: i18n.developer,
|
||||
class: "dev-link enabled"
|
||||
})
|
||||
];
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const renderLegacyLink = () => {
|
||||
const legacyMod = getConfig().modules.legacyMod === "on";
|
||||
if (legacyMod) {
|
||||
|
|
@ -826,6 +780,19 @@ const renderProjectsLink = () => {
|
|||
: "";
|
||||
};
|
||||
|
||||
const renderIndustryLink = () => {
|
||||
const industryMod = getConfig().modules.industryMod === "on";
|
||||
return industryMod
|
||||
? [
|
||||
navLink({
|
||||
href: "/industry",
|
||||
emoji: "ꖴ",
|
||||
text: i18n.industryTitle
|
||||
})
|
||||
]
|
||||
: "";
|
||||
};
|
||||
|
||||
const renderBankingLink = () => {
|
||||
const bankingMod = getConfig().modules.bankingMod === "on";
|
||||
return bankingMod
|
||||
|
|
@ -1134,6 +1101,8 @@ const renderTasksLink = () => {
|
|||
: "";
|
||||
};
|
||||
|
||||
|
||||
// === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + hive ===
|
||||
const renderMobileTopbar = () => div(
|
||||
{ class: "oasis-mobile-topbar" },
|
||||
a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })),
|
||||
|
|
@ -1223,16 +1192,6 @@ const renderBottomBar = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const RADIAL_ITEMS = [
|
||||
{ key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null },
|
||||
{ key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
|
||||
{ key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' },
|
||||
{ key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' },
|
||||
{ key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' },
|
||||
{ key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' },
|
||||
{ key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' },
|
||||
];
|
||||
|
||||
const renderHiveNav = () => {
|
||||
if (process.env.OASIS_MOBILE !== '1') return "";
|
||||
const cfg = getConfig();
|
||||
|
|
@ -1299,14 +1258,9 @@ const template = (titlePrefix, ...elements) => {
|
|||
const currentConfig = getConfig();
|
||||
const theme = currentConfig.themes.current || "Dark-SNH";
|
||||
const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : "blocks";
|
||||
const baseTheme = theme === "OasisMobileUX" ? "Dark-SNH" : theme;
|
||||
const mobileUXActive = (process.env.OASIS_MOBILE === '1') || (theme === "OasisMobileUX");
|
||||
const mobileUXLink = mobileUXActive
|
||||
? link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) })
|
||||
: "";
|
||||
const themeLink = link({
|
||||
rel: "stylesheet",
|
||||
href: `/assets/themes/${baseTheme}.css`
|
||||
href: `/assets/themes/${theme}.css`
|
||||
});
|
||||
const nodes = html(
|
||||
{ lang: "en" },
|
||||
|
|
@ -1315,7 +1269,6 @@ const template = (titlePrefix, ...elements) => {
|
|||
link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
|
||||
themeLink,
|
||||
link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }),
|
||||
mobileUXLink,
|
||||
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
||||
meta({ charset: "utf-8" }),
|
||||
meta({ name: "description", content: i18n.oasisDescription }),
|
||||
|
|
@ -1323,8 +1276,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
name: "viewport",
|
||||
content: toAttributes({
|
||||
width: "device-width",
|
||||
"initial-scale": 1,
|
||||
"viewport-fit": "cover"
|
||||
"initial-scale": 1
|
||||
})
|
||||
})
|
||||
),
|
||||
|
|
@ -1334,50 +1286,6 @@ const template = (titlePrefix, ...elements) => {
|
|||
div(
|
||||
{ class: uxMode === "ainav" ? "header ainav-only" : "header" },
|
||||
renderMobileTopbar(),
|
||||
div(
|
||||
{ class: "top-bar-left" },
|
||||
a(
|
||||
{ class: "logo-icon", href: "/" },
|
||||
img({
|
||||
class: "logo-icon",
|
||||
src: "/assets/images/snh-oasis.jpg",
|
||||
alt: "Oasis Logo"
|
||||
})
|
||||
),
|
||||
uxMode === "ainav" ? nav(
|
||||
ul(
|
||||
(() => {
|
||||
const inboxCount = sharedState.getInboxCount();
|
||||
const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '';
|
||||
return li(
|
||||
a({ href: "/inbox" },
|
||||
span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge
|
||||
)
|
||||
);
|
||||
})(),
|
||||
navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }),
|
||||
navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites })
|
||||
)
|
||||
) : nav(
|
||||
ul(
|
||||
(() => {
|
||||
const inboxCount = sharedState.getInboxCount();
|
||||
const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '';
|
||||
return li(
|
||||
a({ href: "/inbox" },
|
||||
span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge
|
||||
)
|
||||
);
|
||||
})(),
|
||||
navLink({
|
||||
href: "/pm",
|
||||
emoji: "ꕕ",
|
||||
text: i18n.privateMessage
|
||||
}),
|
||||
navLink({ href: "/publish", emoji: "❂", text: i18n.publish })
|
||||
)
|
||||
)
|
||||
),
|
||||
div(
|
||||
{ class: "oasis-mobile-menus" },
|
||||
label({ for: "nav-left-toggle", class: "oasis-menu-btn" },
|
||||
|
|
@ -1404,26 +1312,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
button({ type: 'submit', class: 'ai-ask-btn' }, '➤')
|
||||
)
|
||||
);
|
||||
})(),
|
||||
uxMode === "ainav" ? div(
|
||||
{ class: "top-bar-right" },
|
||||
nav(
|
||||
ul(
|
||||
navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }),
|
||||
navLink({ href: "/graphos", emoji: "ꕢ", text: i18n.graphos }),
|
||||
navLink({ href: "/peers", emoji: "⧖", text: i18n.peers })
|
||||
)
|
||||
)
|
||||
) : div(
|
||||
{ class: "top-bar-right" },
|
||||
nav(
|
||||
ul(
|
||||
navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle }),
|
||||
renderGraphosLink(),
|
||||
navLink({ href: "/peers", emoji: "⧖", text: i18n.peers })
|
||||
)
|
||||
)
|
||||
)
|
||||
})()
|
||||
),
|
||||
(() => {
|
||||
const updateFlagPath = path.join(__dirname, '../server/.update_required');
|
||||
|
|
@ -1437,6 +1326,24 @@ const template = (titlePrefix, ...elements) => {
|
|||
}
|
||||
return null;
|
||||
})(),
|
||||
(() => {
|
||||
try {
|
||||
const onboarding = require('../models/onboarding_model');
|
||||
if (!onboarding.bannerVisible(config && config.path)) return null;
|
||||
return div(
|
||||
{ class: "update-banner welcome-banner" },
|
||||
span({ class: "update-banner-icon" }, "🌴"),
|
||||
span({ class: "update-banner-text" }, i18n.welcomeBannerText),
|
||||
a({ href: "/welcome", class: "update-banner-link" }, i18n.welcomeBannerAction),
|
||||
form(
|
||||
{ method: "POST", action: "/welcome/dismiss", class: "welcome-banner-close" },
|
||||
button({ type: "submit", class: "welcome-banner-close-btn" }, "✕")
|
||||
)
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
div(
|
||||
{ class: uxMode === "ainav" ? "main-content ainav-only" : "main-content" },
|
||||
uxMode === "ainav" ? null : div(
|
||||
|
|
@ -1511,6 +1418,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
renderBankingLink(),
|
||||
renderMarketLink(),
|
||||
renderProjectsLink(),
|
||||
renderIndustryLink(),
|
||||
renderJobsLink(),
|
||||
renderShopsLink(),
|
||||
renderTransfersLink()
|
||||
|
|
@ -1527,6 +1435,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
emoji: "ꖸ",
|
||||
text: i18n.blockchain
|
||||
}),
|
||||
renderDevLink(),
|
||||
renderCipherLink(),
|
||||
renderInvitesLink(),
|
||||
renderLegacyLink(),
|
||||
|
|
@ -1618,7 +1527,6 @@ const template = (titlePrefix, ...elements) => {
|
|||
)
|
||||
),
|
||||
renderBottomBar(),
|
||||
script({ src: "/js/oasis-mobile-nav.js", defer: "defer" }),
|
||||
renderFooter()
|
||||
)
|
||||
);
|
||||
|
|
@ -1628,23 +1536,69 @@ const template = (titlePrefix, ...elements) => {
|
|||
|
||||
exports.template = template;
|
||||
|
||||
exports.bottomBarPickerView = () => {
|
||||
const cfg = getConfig();
|
||||
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
|
||||
const MAX = 4;
|
||||
const pinnedKeys = cfg.bottomBarPins || [];
|
||||
const pinned = pinnedKeys
|
||||
.map(key => exports.PINNABLE_MODULES.find(m => m.key === key))
|
||||
.filter(Boolean);
|
||||
const full = pinned.length >= MAX;
|
||||
const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls },
|
||||
input({ type: 'hidden', name: 'action', value: action }),
|
||||
input({ type: 'hidden', name: 'key', value: key }),
|
||||
node
|
||||
);
|
||||
const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form',
|
||||
button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' },
|
||||
span({ class: 'bb-manage-ico' }, m.glyph),
|
||||
span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key),
|
||||
span({ class: 'bb-manage-x' }, '✕')
|
||||
)
|
||||
);
|
||||
const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form',
|
||||
button({ type: 'submit', class: 'bb-pick-btn' },
|
||||
span({ class: 'bb-pick-ico' }, m.glyph),
|
||||
span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key)
|
||||
)
|
||||
);
|
||||
const addable = exports.PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key));
|
||||
return template(
|
||||
i18n.bbManage || 'Edit bar',
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h2(i18n.bbPickTitle || 'Customize the bar'),
|
||||
p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.')
|
||||
),
|
||||
div({ class: 'bb-manage' },
|
||||
h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`),
|
||||
pinned.length
|
||||
? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow))
|
||||
: p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None')
|
||||
),
|
||||
div({ class: 'bb-manage-add' },
|
||||
h3(i18n.bbAdd || 'Add'),
|
||||
full
|
||||
? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached')
|
||||
: div({ class: 'bb-pick-grid' }, ...addable.map(addBtn))
|
||||
),
|
||||
a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done')
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.ainavHomeView = ({ recentTags = [] } = {}) => {
|
||||
const currentConfig = getConfig();
|
||||
const theme = currentConfig.themes.current || "Dark-SNH";
|
||||
const baseTheme = theme === "OasisMobileUX" ? "Dark-SNH" : theme;
|
||||
const mobileUXActive = (process.env.OASIS_MOBILE === '1') || (theme === "OasisMobileUX");
|
||||
const mobileUXLink = mobileUXActive
|
||||
? link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) })
|
||||
: "";
|
||||
const placeholder = i18n.aiNavPlaceholder || 'Where do you want to go?';
|
||||
const nodes = html(
|
||||
{ lang: "en" },
|
||||
head(
|
||||
title(placeholder, " | Oasis"),
|
||||
link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
|
||||
link({ rel: "stylesheet", href: `/assets/themes/${baseTheme}.css` }),
|
||||
link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }),
|
||||
link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }),
|
||||
mobileUXLink,
|
||||
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
||||
meta({ charset: "utf-8" }),
|
||||
meta({ name: "description", content: i18n.oasisDescription }),
|
||||
|
|
@ -3156,7 +3110,12 @@ exports.mentionsView = ({ messages, myFeedId }) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
||||
exports.privateView = async (messagesInput, filter, decrypted = null, notice = '') => {
|
||||
const noticeText = notice === 'unavailable'
|
||||
? (i18n.fileShareUnavailable || 'This file is not available right now. Try again later.')
|
||||
: notice === 'badkey'
|
||||
? (i18n.pmCrypterBadKey || 'Your shared key is incorrect!')
|
||||
: ''
|
||||
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()
|
||||
|
|
@ -3169,7 +3128,8 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const hrefFor = {
|
||||
job: (id) => `/jobs/${encodeURIComponent(id)}`,
|
||||
project: (id) => `/projects/${encodeURIComponent(id)}`,
|
||||
market: (id) => `/market/${encodeURIComponent(id)}`
|
||||
market: (id) => `/market/${encodeURIComponent(id)}`,
|
||||
shopProduct: (id) => `/shops/product/${encodeURIComponent(id)}`
|
||||
}
|
||||
|
||||
const clickableCardProps = (href, extraClass = '') => {
|
||||
|
|
@ -3184,6 +3144,15 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
|
||||
const chip = (txt) => span({ class: 'chip' }, txt)
|
||||
|
||||
const fmtBytes = (n) => {
|
||||
const b = Number(n) || 0
|
||||
if (b < 1024) return `${b} B`
|
||||
const units = ['KB', 'MB', 'GB', 'TB']
|
||||
let v = b / 1024, i = 0
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
||||
return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}`
|
||||
}
|
||||
|
||||
const e2eChip = () => span({ class: 'pm-exposition-chip pm-exposition-encrypted' },
|
||||
span({ class: 'pm-exposition-icon' }, '🔒'),
|
||||
span({ class: 'pm-exposition-text' }, i18n.encryptedChipLabel || 'E2E')
|
||||
|
|
@ -3229,17 +3198,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
try { return Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) { return 0; }
|
||||
}
|
||||
|
||||
function bubbleMeta({ sentAt, subjectRaw, crypter, msgSize, msgKey }) {
|
||||
const ecoChip = msgSize ? renderEcoTax(msgSize, msgKey) : null
|
||||
return div({ class: 'pm-bubble-meta' },
|
||||
subjectRaw ? span({ class: 'pm-bubble-subject' }, subjectRaw) : null,
|
||||
span({ class: 'pm-bubble-time' }, moment(sentAt).format('DD/MM HH:mm')),
|
||||
span({ class: 'pm-bubble-lock', title: crypter ? '2xE2E' : 'E2E' }, crypter ? '🔒🔒' : '🔒'),
|
||||
ecoChip ? span({ class: 'pm-bubble-eco' }, ecoChip) : null
|
||||
)
|
||||
}
|
||||
|
||||
function actions({ key, replyId, subjectRaw, text }) {
|
||||
function actions({ key, replyId, subjectRaw, text, extra = null }) {
|
||||
const stop = { onclick: 'event.stopPropagation()' }
|
||||
const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}`
|
||||
const isSelf = replyId === userId
|
||||
|
|
@ -3250,6 +3209,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
input({ type: 'hidden', name: 'quote', value: text || '' }),
|
||||
button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase())
|
||||
),
|
||||
extra || null,
|
||||
form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop },
|
||||
button({ type: 'submit', class: 'pm-btn delete-btn danger-btn' }, i18n.privateDelete.toUpperCase())
|
||||
)
|
||||
|
|
@ -3293,6 +3253,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/)
|
||||
return m ? m[1] : ''
|
||||
}
|
||||
if (kind === 'shopProduct') {
|
||||
const m = str.match(/\/shops\/product\/([%A-Za-z0-9/+._=-]+\.sha256)/)
|
||||
return m ? m[1] : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
@ -3315,15 +3279,27 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
}
|
||||
}
|
||||
flushQuote()
|
||||
return parts.join('<br>')
|
||||
const mdLinks = []
|
||||
const masked = parts.join('<br>')
|
||||
.replace(/\[([^\]\n]{1,120})\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)/g, (match, label, href) => {
|
||||
const idx = mdLinks.length
|
||||
const safeHref = String(href).replace(/"/g, '%22')
|
||||
const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener noreferrer"' : ''
|
||||
mdLinks.push(`<a class="oasis-md-link" href="${safeHref}"${ext}>${label}</a>`)
|
||||
return `\u0000MD${idx}\u0000`
|
||||
})
|
||||
return masked
|
||||
.replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `<a class="user-link" href="/author/${encodeURIComponent(id)}">${userLinkLabel(id)}</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>`)
|
||||
.replace(/\/calendars\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="calendar-link" href="/calendars/${encodeURIComponent(id)}">${match}</a>`)
|
||||
.replace(/\/ai\/ask\?[^\s<"]+/g, (match) => `<a class="ai-ask-link" href="${match}">${match}</a>`)
|
||||
.replace(/(?<![A-Za-z0-9_])\/(profile|inbox|invites|peers|tribes|inhabitants|publish|activity|settings|modules|banking|larp|melody|audios|videos|images|documents|bookmarks|torrents|forum|feed|fediverse|events|tasks|votes|reports|market|jobs|projects|shops|pixelia|opinions|trending|agenda|cv|favorites|stats|blockexplorer|wallet|chats|pads|maps|calendars|ai|games)(?![A-Za-z0-9_\/])/g, (match) => `<a class="oasis-path-link" href="${match}">${match}</a>`)
|
||||
.replace(/\/tribe\/([%A-Za-z0-9/+._=-]+\.sha256)(\?section=[a-zA-Z]+)?/g, (match) => `<a class="tribe-link" href="${match}">${match}</a>`)
|
||||
.replace(/\/larp\/([a-zA-Z]+)/g, (match) => `<a class="larp-link" href="${match}">${match}</a>`)
|
||||
.replace(/(?<![A-Za-z0-9_])\/(profile|inbox|invites|peers|tribes|inhabitants|publish|activity|settings|modules|banking|larp|parliament|courts|melody|audios|videos|images|documents|bookmarks|torrents|forum|feed|fediverse|events|tasks|votes|reports|market|jobs|projects|industry|shops|pixelia|opinions|trending|agenda|cv|favorites|stats|blockexplorer|wallet|chats|pads|maps|calendars|ai|games|search)(?![A-Za-z0-9_\/])/g, (match) => `<a class="oasis-path-link" href="${match}">${match}</a>`)
|
||||
.replace(/(https?:\/\/[^\s<"]+)/g, (match) => `<a href="${match}" target="_blank" rel="noopener noreferrer">${match}</a>`)
|
||||
.replace(/\u0000MD(\d+)\u0000/g, (m, i) => mdLinks[Number(i)] !== undefined ? mdLinks[Number(i)] : m)
|
||||
}
|
||||
|
||||
const threads = {}
|
||||
|
|
@ -3374,7 +3350,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const href = jobId ? hrefFor.job(jobId) : null
|
||||
return div(
|
||||
clickableCardProps(href, `job-notification thread-level-0`),
|
||||
headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }),
|
||||
headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`),
|
||||
p(
|
||||
i18n.pmInhabitantWithId, ' ',
|
||||
|
|
@ -3398,7 +3374,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const href = projectId ? hrefFor.project(projectId) : null
|
||||
return div(
|
||||
clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`),
|
||||
headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }),
|
||||
headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`),
|
||||
p(
|
||||
i18n.pmInhabitantWithId, ' ',
|
||||
|
|
@ -3412,23 +3388,28 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
)
|
||||
}
|
||||
|
||||
function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize }) {
|
||||
function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize, variant = 'market' }) {
|
||||
const isShop = variant === 'shop'
|
||||
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 price = (text.match(/for:\s*\$?([\d.]+)/) || [])[1] || ''
|
||||
const marketId = pickLink(text, 'market')
|
||||
const href = marketId ? hrefFor.market(marketId) : null
|
||||
const shopProductId = pickLink(text, 'shopProduct')
|
||||
const href = isShop ? (shopProductId ? hrefFor.shopProduct(shopProductId) : null) : (marketId ? hrefFor.market(marketId) : null)
|
||||
const icon = isShop ? '🛍️' : '💰'
|
||||
const botLabel = isShop ? i18n.pmBotShops : i18n.pmBotMarket
|
||||
const soldTitle = isShop ? (i18n.inboxShopSoldTitle || 'Product Sold') : i18n.inboxMarketItemSoldTitle
|
||||
return div(
|
||||
clickableCardProps(href, 'market-sold-notification thread-level-0'),
|
||||
headerLine({ sentAt, from, toLinks, subject, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`),
|
||||
clickableCardProps(href, (isShop ? 'shop-sold-notification' : 'market-sold-notification') + ' thread-level-0'),
|
||||
headerLine({ sentAt, from, toLinks, subject: soldTitle, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `${icon} ${botLabel} · ${soldTitle}`),
|
||||
p(
|
||||
i18n.pmYourItem, ' ',
|
||||
href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
|
||||
href ? a({ class: isShop ? 'shop-link' : 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
|
||||
' ',
|
||||
i18n.pmHasBeenSoldTo, ' ',
|
||||
linkAuthor(buyerId),
|
||||
price ? ` ${i18n.pmFor} $${price}.` : '.'
|
||||
price ? ` ${i18n.pmFor} ${price} ECO.` : '.'
|
||||
),
|
||||
actions({ key, replyId: buyerId, subjectRaw: itemTitle, text })
|
||||
)
|
||||
|
|
@ -3441,7 +3422,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const href = projectId ? hrefFor.project(projectId) : null
|
||||
return div(
|
||||
clickableCardProps(href, 'project-pledge-notification thread-level-0'),
|
||||
headerLine({ sentAt, from, toLinks, subject: 'PROJECT_PLEDGE', msgKey: key, msgSize }),
|
||||
headerLine({ sentAt, from, toLinks, subject: i18n.inboxProjectPledgedTitle, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`),
|
||||
p(
|
||||
i18n.pmInhabitantWithId, ' ',
|
||||
|
|
@ -3455,6 +3436,41 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
)
|
||||
}
|
||||
|
||||
function PoliticalBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) {
|
||||
const titleMap = {
|
||||
LARP_RULING: i18n.politicalBotLarpTitle || 'The ruling house of the L.A.R.P has changed.',
|
||||
PARLIAMENT_GOV: i18n.politicalBotParliamentTitle || 'A new government cycle has begun in the Parliament.',
|
||||
TRIBE_GOV: i18n.politicalBotTribeTitle || 'A new government cycle has begun in one of your Tribes.'
|
||||
}
|
||||
const title = titleMap[subjectU] || (i18n.pmBotPolitical || 'PoliticalBot')
|
||||
return div(
|
||||
{ class: 'pm-card political-bot-notification thread-level-0' },
|
||||
headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `🏛️ ${i18n.pmBotPolitical} · ${title}`),
|
||||
div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text || '')) }),
|
||||
actions({ key, replyId: from, subjectRaw: title, text })
|
||||
)
|
||||
}
|
||||
|
||||
function IndustryBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) {
|
||||
const titleMap = {
|
||||
INDUSTRY_ADMITTED: i18n.industryBotAdmittedTitle || 'You have been admitted to a facility.',
|
||||
INDUSTRY_APPLICATION: i18n.industryBotApplicationTitle || 'New membership application in your facility.',
|
||||
INDUSTRY_INVITED: i18n.industryBotInvitedTitle || 'You have been invited to a facility.',
|
||||
INDUSTRY_DISSOLVED: i18n.industryBotDissolvedTitle || 'A facility you belong to has been dissolved.',
|
||||
INDUSTRY_BUILD_APPROVED: i18n.industryBotBuildApprovedTitle || 'A build has been approved.',
|
||||
INDUSTRY_DISTRIBUTED: i18n.industryBotDistributedTitle || 'A build output has been distributed.'
|
||||
}
|
||||
const title = titleMap[subjectU] || (i18n.pmBotIndustry || 'IndustryBot')
|
||||
return div(
|
||||
{ class: 'pm-card industry-bot-notification thread-level-0' },
|
||||
headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }),
|
||||
h2({ class: 'pm-title' }, `🏭 ${i18n.pmBotIndustry || 'IndustryBot'} · ${title}`),
|
||||
div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text || '')) }),
|
||||
actions({ key, replyId: from, subjectRaw: title, text })
|
||||
)
|
||||
}
|
||||
|
||||
const { renderEncryptedChip } = require('./clearnet_view');
|
||||
return template(
|
||||
i18n.private,
|
||||
|
|
@ -3463,6 +3479,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
div({ class: 'title-with-chip' }, h2(i18n.private), renderEncryptedChip(i18n)),
|
||||
p(i18n.privateDescription)
|
||||
),
|
||||
noticeText ? div({ class: 'pm-form-error-msg' }, p('✗ ' + noticeText)) : null,
|
||||
(() => {
|
||||
const pmVis = getConfig().pmVisibility === 'mutuals' ? 'mutuals' : 'whole'
|
||||
const pmVisLabel = pmVis === 'mutuals' ? i18n.settingsPmVisibilityMutuals : i18n.settingsPmVisibilityWhole
|
||||
|
|
@ -3526,7 +3543,35 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : []
|
||||
const level = threadLevel(subjectRaw)
|
||||
const msgSize = msgSizeBytes(msg)
|
||||
const isMob = process.env.OASIS_MOBILE === '1'
|
||||
|
||||
if (content.fileShare && typeof content.fileShare === 'object') {
|
||||
const fsp = content.fileShare
|
||||
const dblKey = (decrypted && decrypted.key === msg.key) ? decrypted : null
|
||||
return div(
|
||||
{ class: 'pm-card normal-pm pm-fileshare-card' },
|
||||
headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: !!fsp.crypter }),
|
||||
div({ class: 'pm-fileshare-info' },
|
||||
span({ class: 'pm-fileshare-icon' }, '📎'),
|
||||
span({ class: 'pm-fileshare-name' }, String(fsp.filename || 'file')),
|
||||
span({ class: 'pm-fileshare-meta' }, `${fmtBytes(fsp.size)} · ${String(fsp.mime || 'application/octet-stream')}`)
|
||||
),
|
||||
fsp.crypter
|
||||
? form({ method: 'GET', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-fileshare-download-form' },
|
||||
dblKey && dblKey.error ? div({ class: 'pm-form-error-msg' }, p('✗ ' + (i18n.pmCrypterBadKey || 'Your shared key is incorrect!'))) : null,
|
||||
input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }),
|
||||
button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase())
|
||||
)
|
||||
: null,
|
||||
actions({
|
||||
key: msg.key, replyId: fromResolved, subjectRaw, text: '',
|
||||
extra: fsp.crypter
|
||||
? null
|
||||
: form({ method: 'GET', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-action-form', onclick: 'event.stopPropagation()' },
|
||||
button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase())
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') {
|
||||
return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize })
|
||||
|
|
@ -3535,7 +3580,16 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize })
|
||||
}
|
||||
if (subjectU === 'MARKET_SOLD') {
|
||||
return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize })
|
||||
return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'market' })
|
||||
}
|
||||
if (subjectU === 'SHOP_SOLD') {
|
||||
return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'shop' })
|
||||
}
|
||||
if (subjectU === 'LARP_RULING' || subjectU === 'PARLIAMENT_GOV' || subjectU === 'TRIBE_GOV') {
|
||||
return PoliticalBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize })
|
||||
}
|
||||
if (subjectU === 'INDUSTRY_ADMITTED' || subjectU === 'INDUSTRY_APPLICATION' || subjectU === 'INDUSTRY_INVITED' || subjectU === 'INDUSTRY_DISSOLVED' || subjectU === 'INDUSTRY_BUILD_APPROVED' || subjectU === 'INDUSTRY_DISTRIBUTED') {
|
||||
return IndustryBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize })
|
||||
}
|
||||
if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') {
|
||||
return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key, msgSize })
|
||||
|
|
@ -3544,48 +3598,23 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
if (content.crypter === true) {
|
||||
const dec = (decrypted && decrypted.key === msg.key) ? decrypted : null
|
||||
const shownText = dec && typeof dec.text === 'string' ? dec.text : ''
|
||||
const cbody = dec && typeof dec.text === 'string'
|
||||
? div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(dec.text)) })
|
||||
: dec && dec.error
|
||||
? div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.pmCrypterBadKey))
|
||||
: null
|
||||
const cform = dec && typeof dec.text === 'string'
|
||||
? null
|
||||
: form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' },
|
||||
input({ type: 'hidden', name: 'id', value: msg.key }),
|
||||
input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }),
|
||||
input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }),
|
||||
button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase())
|
||||
)
|
||||
const cacts = actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText })
|
||||
if (isMob) {
|
||||
const mine = isSent(msg)
|
||||
return div(
|
||||
{ class: `pm-card normal-pm pm-crypter-card pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` },
|
||||
mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)),
|
||||
cbody,
|
||||
cform,
|
||||
bubbleMeta({ sentAt, subjectRaw, crypter: true, msgSize, msgKey: msg.key }),
|
||||
cacts
|
||||
)
|
||||
}
|
||||
return div(
|
||||
{ class: 'pm-card normal-pm pm-crypter-card' },
|
||||
headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: true }),
|
||||
cbody,
|
||||
cform,
|
||||
cacts
|
||||
)
|
||||
}
|
||||
|
||||
if (isMob) {
|
||||
const mine = isSent(msg)
|
||||
return div(
|
||||
{ class: `pm-card normal-pm pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` },
|
||||
mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)),
|
||||
div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }),
|
||||
bubbleMeta({ sentAt, subjectRaw, crypter: false, msgSize, msgKey: msg.key }),
|
||||
actions({ key: msg.key, replyId: fromResolved, subjectRaw, text })
|
||||
dec && typeof dec.text === 'string'
|
||||
? div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(dec.text)) })
|
||||
: dec && dec.error
|
||||
? div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.pmCrypterBadKey))
|
||||
: null,
|
||||
dec && typeof dec.text === 'string'
|
||||
? null
|
||||
: form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' },
|
||||
input({ type: 'hidden', name: 'id', value: msg.key }),
|
||||
input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }),
|
||||
input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }),
|
||||
button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase())
|
||||
),
|
||||
actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3608,7 +3637,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
|
|||
threadGroups[tid].push(msg)
|
||||
}
|
||||
|
||||
if (!threadOrder.length) return emptyState({ icon: "✉", title: i18n.noPrivateMessages, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta })
|
||||
if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages)
|
||||
|
||||
return threadOrder.map(tid => {
|
||||
const msgs = threadGroups[tid]
|
||||
|
|
@ -3741,7 +3770,7 @@ exports.publishView = (preview, text, contentWarning) => {
|
|||
cols: "50",
|
||||
placeholder: i18n.publishWarningPlaceholder,
|
||||
class: "publish-textarea",
|
||||
maxlength: "8096"
|
||||
maxlength: "7000"
|
||||
},
|
||||
text || ""
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue