UX/UI redesign for the mobile WebView
- Compact top bar (logo, OASIS, user avatar with default fallback). - Fixed bottom navigation (Search, Inbox, Write, Activity + Peers/Invites) with active state. - Removed duplicated header nav buttons on mobile. - Personal/Community submenus: single-open accordion. - Activity filters grouped into quick modes + collapsible thematic groups. - Private messages rendered as chat bubbles on mobile. - Text fixes, full-width stacked tables, touch feedback; Activity group labels in 11 locales. Mobile-scoped (mobile.css + views); desktop unaffected.
This commit is contained in:
parent
e563b86ede
commit
ea5f5cbcc2
16 changed files with 847 additions and 35 deletions
|
|
@ -1,4 +1,4 @@
|
|||
const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary, ul, li, label } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views');
|
||||
const opinionCategories = require('../backend/opinion_categories');
|
||||
|
||||
|
|
@ -1841,25 +1841,43 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
h2(i18n.activityList),
|
||||
p(desc)
|
||||
),
|
||||
div({ class: 'activity-filter-grid' },
|
||||
...[
|
||||
activityTypes.slice(0, 3),
|
||||
activityTypes.slice(3, 8),
|
||||
activityTypes.slice(8, 13),
|
||||
activityTypes.slice(13, 19),
|
||||
activityTypes.slice(19, 26),
|
||||
activityTypes.slice(26)
|
||||
].map(col =>
|
||||
div({ class: 'activity-filter-col' },
|
||||
col.map(({ type, label }) =>
|
||||
form({ method: 'GET', action: '/activity' },
|
||||
input({ type: 'hidden', name: 'filter', value: type }),
|
||||
button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
|
||||
)
|
||||
)
|
||||
(() => {
|
||||
const labelOf = {};
|
||||
activityTypes.forEach(t => { labelOf[t.type] = t.label; });
|
||||
const mkBtn = (type) => form({ method: 'GET', action: '/activity' },
|
||||
input({ type: 'hidden', name: 'filter', value: type }),
|
||||
button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, labelOf[type] || type)
|
||||
);
|
||||
const QUICK = ['recent', 'all', 'mine'];
|
||||
const GROUPS = [
|
||||
{ id: 'act-comunidad', emoji: '👥', title: i18n.activityGroupCommunity || 'Community & Governance', types: ['inhabitants', 'tribe', 'larp', 'parliament', 'courts', 'votes', 'report'] },
|
||||
{ id: 'act-organizacion', emoji: '🗓', title: i18n.activityGroupOrg || 'Organization', types: ['event', 'calendar', 'task', 'project', 'job'] },
|
||||
{ id: 'act-comunicacion', emoji: '💬', title: i18n.activityGroupComm || 'Communication', types: ['post', 'feed', 'chat', 'pad', 'forum'] },
|
||||
{ id: 'act-economia', emoji: '💰', title: i18n.activityGroupEconomy || 'Economy', types: ['banking', 'market', 'shop', 'transfer'] },
|
||||
{ id: 'act-media', emoji: '🎬', title: i18n.activityGroupMedia || 'Content & Media', types: ['audio', 'bookmark', 'document', 'image', 'torrent', 'video'] },
|
||||
{ id: 'act-otros', emoji: '⋯', title: i18n.activityGroupOther || 'Others', types: ['map', 'curriculum'] },
|
||||
];
|
||||
return div({ class: 'activity-filter-acc' },
|
||||
div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)),
|
||||
ul({ class: 'oasis-nav-groups activity-groups' },
|
||||
GROUPS.map(g => {
|
||||
const openByFilter = g.types.includes(filter);
|
||||
return li({ class: 'oasis-nav-group' },
|
||||
input({ type: 'checkbox', id: `oasis-nav-group-${g.id}`, class: 'oasis-nav-toggle', ...(openByFilter ? { checked: true } : {}) }),
|
||||
label({ for: `oasis-nav-group-${g.id}`, class: 'oasis-nav-header' },
|
||||
span({ class: 'emoji' }, g.emoji),
|
||||
span({ class: 'nav-text' }, g.title),
|
||||
span({ class: 'act-count' }, String(g.types.length)),
|
||||
span({ class: 'oasis-nav-arrow' }, '▾')
|
||||
),
|
||||
ul({ class: 'oasis-nav-list' },
|
||||
g.types.map(t => li(mkBtn(t)))
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
})(),
|
||||
sub
|
||||
? div({ class: 'activity-sub-filter' },
|
||||
sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase()))
|
||||
|
|
|
|||
|
|
@ -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, 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, script, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");
|
||||
|
||||
const lodash = require("../server/node_modules/lodash");
|
||||
const markdown = require("./markdown");
|
||||
|
|
@ -1073,6 +1073,34 @@ const renderTasksLink = () => {
|
|||
: "";
|
||||
};
|
||||
|
||||
const renderMobileTopbar = () => div(
|
||||
{ class: "oasis-mobile-topbar" },
|
||||
a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })),
|
||||
a({ class: "omt-title", href: "/" }, "OASIS"),
|
||||
a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" }))
|
||||
);
|
||||
|
||||
const renderBottomBar = () => {
|
||||
const inboxN = sharedState.getInboxCount();
|
||||
const bbLink = (href, ico, label, badge) => a(
|
||||
{ class: "bb-item", href },
|
||||
span({ class: "bb-ico" }, ico),
|
||||
(badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "",
|
||||
span({ class: "bb-lbl" }, label)
|
||||
);
|
||||
return nav(
|
||||
{ class: "oasis-bottombar", style: "display:none", "aria-label": "Quick actions" },
|
||||
bbLink("/search", "ꔅ", i18n.searchTitle || "Buscar"),
|
||||
bbLink("/inbox", "☂", i18n.inbox || "Inbox", inboxN),
|
||||
bbLink("/publish", "❂", i18n.publish || "Write"),
|
||||
bbLink("/activity", "ꔙ", i18n.activityTitle || "Activity"),
|
||||
div({ class: "bb-pair" },
|
||||
bbLink("/peers", "⧖", i18n.peers || "Peers"),
|
||||
bbLink("/invites", "ꔮ", i18n.invites || "Invites")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const template = (titlePrefix, ...elements) => {
|
||||
const currentConfig = getConfig();
|
||||
const theme = currentConfig.themes.current || "Dark-SNH";
|
||||
|
|
@ -1095,7 +1123,8 @@ const template = (titlePrefix, ...elements) => {
|
|||
name: "viewport",
|
||||
content: toAttributes({
|
||||
width: "device-width",
|
||||
"initial-scale": 1
|
||||
"initial-scale": 1,
|
||||
"viewport-fit": "cover"
|
||||
})
|
||||
})
|
||||
),
|
||||
|
|
@ -1104,6 +1133,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
input({ type: "checkbox", id: "nav-right-toggle", class: "oasis-nav-side-toggle" }),
|
||||
div(
|
||||
{ class: uxMode === "ainav" ? "header ainav-only" : "header" },
|
||||
renderMobileTopbar(),
|
||||
div(
|
||||
{ class: "top-bar-left" },
|
||||
a(
|
||||
|
|
@ -1387,6 +1417,8 @@ const template = (titlePrefix, ...elements) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
renderBottomBar(),
|
||||
script({ src: "/js/oasis-mobile-nav.js", defer: "defer" }),
|
||||
renderFooter()
|
||||
)
|
||||
);
|
||||
|
|
@ -2991,6 +3023,16 @@ 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 }) {
|
||||
const stop = { onclick: 'event.stopPropagation()' }
|
||||
const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}`
|
||||
|
|
@ -3278,6 +3320,7 @@ 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 (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') {
|
||||
return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize })
|
||||
|
|
@ -3295,23 +3338,48 @@ 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 }),
|
||||
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 })
|
||||
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 })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue