[0.9.1] Mobile: fixed topbar, global hive, per-message chat reply/switcher, forum subtopics, square module grid
- Topbar: single fixed line [logo][Personal][Community][avatar] in one dark tone (#121212) with gold accents only; the hive hexagons scroll below it. - Hive: Personal/Community filter the home hive from any page; opening a category shows its modules as a 2x2 grid of square buttons (a lone odd button is centered instead of left-aligned). - Chats: per-message reply (?replyTo=), chat switcher strip and quote banner; the message rate limit is lifted on mobile. - Forums: optional subtopics (?topic=) with threaded rendering. - Bottombar: fixed Inbox - PM - Write - Search - Graphos - Peers.
This commit is contained in:
parent
2b2c65fc7a
commit
ed9f01e8e6
8 changed files with 280 additions and 129 deletions
|
|
@ -1993,7 +1993,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
h2(i18n.activityList),
|
||||
p(desc)
|
||||
),
|
||||
renderHiveNav(),
|
||||
input({ type: 'checkbox', id: 'activity-filter-toggle', class: 'activity-filter-toggle' }),
|
||||
label({ for: 'activity-filter-toggle', class: 'activity-filter-btn' },
|
||||
span(i18n.filter || 'Filter'), span({ class: 'activity-filter-caret' }, '▾')),
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ const renderMessageText = (text) => {
|
|||
return span({ class: "chat-message-text" }, ...nodes)
|
||||
}
|
||||
|
||||
const renderMessage = (msg, chatAuthor) => {
|
||||
const renderMessage = (msg, chatAuthor, chatKey, msgById) => {
|
||||
const isAuthor = String(msg.author) === String(chatAuthor)
|
||||
const isSelf = String(msg.author) === String(userId)
|
||||
const dateStr = moment(msg.createdAt).format("YYYY/MM/DD HH:mm")
|
||||
|
|
@ -149,11 +149,26 @@ const renderMessage = (msg, chatAuthor) => {
|
|||
|
||||
const imageNode = msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null
|
||||
|
||||
// m\u00f3vil: cita del mensaje respondido + bot\u00f3n \u21a9 para responder a ESTE mensaje (server-side ?replyTo=, sin JS)
|
||||
const mobile = process.env.OASIS_MOBILE === "1"
|
||||
const quoted = (mobile && msg.replyTo && msgById) ? msgById.get(String(msg.replyTo)) : null
|
||||
const quoteNode = quoted
|
||||
? div({ class: "chat-quote" },
|
||||
span({ class: "chat-quote-author" }, quoted.author ? userLink(quoted.author) : span("?")),
|
||||
span({ class: "chat-quote-text" }, String(quoted.text || (quoted.image ? "\ud83d\uddbc" : "")).slice(0, 80))
|
||||
)
|
||||
: null
|
||||
const replyLink = (mobile && chatKey && msg.key)
|
||||
? a({ class: "chat-reply-btn", href: `/chats/${encodeURIComponent(chatKey)}?replyTo=${encodeURIComponent(msg.key)}#chatbox`, title: "Reply" }, "\u21a9")
|
||||
: null
|
||||
|
||||
return div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" },
|
||||
div({ class: "chat-message-meta" },
|
||||
span({ class: "chat-message-sender" }, authorLink),
|
||||
span({ class: "chat-message-date" }, ` [ ${dateStr} ]`)
|
||||
span({ class: "chat-message-date" }, ` [ ${dateStr} ]`),
|
||||
replyLink
|
||||
),
|
||||
quoteNode,
|
||||
imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null,
|
||||
renderMessageText(msg.text || "")
|
||||
)
|
||||
|
|
@ -342,15 +357,26 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
)
|
||||
|
||||
const msgList = safeArr(messages)
|
||||
const msgById = new Map(msgList.map(m => [String(m.key), m]))
|
||||
const replyingTo = (process.env.OASIS_MOBILE === "1" && params.replyTo) ? msgById.get(String(params.replyTo)) : null
|
||||
const canWrite = (isMember || chat.status === "OPEN") && chat.status !== "CLOSED"
|
||||
|
||||
const chatMain = isRestrictedInviteOnly
|
||||
? div({ class: "tribe-main chat-full-width" }, p({ class: "access-denied-msg" }, i18n.chatAccessDenied))
|
||||
: div({ class: "tribe-main chat-full-width" },
|
||||
canWrite
|
||||
? div({ class: "chat-message-form" },
|
||||
? div({ class: "chat-message-form", id: "chatbox" },
|
||||
replyingTo
|
||||
? div({ class: "chat-replying-to" },
|
||||
span({ class: "chat-replying-label" }, "Replying to: "),
|
||||
span({ class: "chat-quote-author" }, replyingTo.author ? userLink(replyingTo.author) : span("?")),
|
||||
span({ class: "chat-quote-text" }, String(replyingTo.text || (replyingTo.image ? "🖼" : "")).slice(0, 80)),
|
||||
a({ class: "chat-reply-cancel", href: `/chats/${encodeURIComponent(chat.key)}`, title: "Cancel" }, "✕")
|
||||
)
|
||||
: null,
|
||||
form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }),
|
||||
replyingTo ? input({ type: "hidden", name: "replyTo", value: String(params.replyTo) }) : null,
|
||||
textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(),
|
||||
span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }), br(), br(),
|
||||
|
|
@ -362,12 +388,24 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
(() => {
|
||||
const visible = msgList.filter(msg => (msg.text && String(msg.text).trim()) || msg.image)
|
||||
return visible.length
|
||||
? visible.map(msg => renderMessage(msg, chat.author))
|
||||
? visible.map(msg => renderMessage(msg, chat.author, chat.key, msgById))
|
||||
: p({ class: "chat-no-messages" }, i18n.chatNoMessages)
|
||||
})()
|
||||
)
|
||||
)
|
||||
|
||||
// móvil: tira para saltar entre chats sin volver a la lista (server-side, enlaces)
|
||||
const switcherChats = process.env.OASIS_MOBILE === "1" && Array.isArray(params.chatList) ? params.chatList : []
|
||||
const chatSwitcher = switcherChats.length
|
||||
? div({ class: "chat-switcher" },
|
||||
switcherChats.slice(0, 30).map(c => a({
|
||||
class: String(c.key) === String(chat.key) ? "chat-switch-chip active" : "chat-switch-chip",
|
||||
href: `/chats/${encodeURIComponent(c.key)}`,
|
||||
title: c.title || ""
|
||||
}, (c.title || "·").slice(0, 18)))
|
||||
)
|
||||
: null
|
||||
|
||||
return template(
|
||||
chat.title || i18n.chatUntitled,
|
||||
section(
|
||||
|
|
@ -375,7 +413,8 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
h2(i18n.chatsTitle),
|
||||
p(i18n.modulesChatsDescription)
|
||||
),
|
||||
renderModeButtons(filter || "all")
|
||||
renderModeButtons(filter || "all"),
|
||||
chatSwitcher
|
||||
),
|
||||
section(
|
||||
div({ class: "tribe-details" },
|
||||
|
|
|
|||
|
|
@ -282,8 +282,20 @@ exports.forumView = async (forums, currentFilter, params = {}) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.singleForumView = async (forum, messagesData, currentFilter) => {
|
||||
exports.singleForumView = async (forum, messagesData, currentFilter, topic = '') => {
|
||||
const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
|
||||
// móvil: subtopics — etiqueta opcional en los mensajes raíz → tira de pestañas + filtro por ?topic=
|
||||
const forumMobile = process.env.OASIS_MOBILE === '1';
|
||||
const rootMsgs = Array.isArray(messagesData.messages) ? messagesData.messages : [];
|
||||
const forumTopics = forumMobile ? [...new Set(rootMsgs.map(m => String(m.subtopic || '').trim()).filter(Boolean))] : [];
|
||||
const activeTopic = forumMobile ? String(topic || '').trim() : '';
|
||||
const shownMsgs = activeTopic ? rootMsgs.filter(m => String(m.subtopic || '').trim() === activeTopic) : rootMsgs;
|
||||
const topicStrip = (forumMobile && forumTopics.length)
|
||||
? div({ class: 'forum-topics' },
|
||||
a({ class: activeTopic ? 'forum-topic-chip' : 'forum-topic-chip active', href: `/forum/${encodeURIComponent(forum.key)}` }, i18n.menuAll || 'All'),
|
||||
...forumTopics.map(t => a({ class: t === activeTopic ? 'forum-topic-chip active' : 'forum-topic-chip', href: `/forum/${encodeURIComponent(forum.key)}?topic=${encodeURIComponent(t)}` }, t))
|
||||
)
|
||||
: null;
|
||||
return template(forum.title,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
|
|
@ -305,6 +317,7 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => {
|
|||
)
|
||||
),
|
||||
div({ class: 'forum-thread-container' },
|
||||
topicStrip,
|
||||
div({
|
||||
class: 'forum-card forum-thread-header',
|
||||
style: 'display:flex;align-items:flex-start;'
|
||||
|
|
@ -394,6 +407,7 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => {
|
|||
action: `/forum/${encodeURIComponent(forum.key)}/message`,
|
||||
class: 'new-message-form'
|
||||
},
|
||||
forumMobile ? input({ type: 'text', name: 'subtopic', value: activeTopic, placeholder: 'Topic (optional)', class: 'forum-topic-input' }) : null,
|
||||
textarea({
|
||||
name: 'message',
|
||||
rows: 4,
|
||||
|
|
@ -408,7 +422,7 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => {
|
|||
}, i18n.forumSendButton)
|
||||
)
|
||||
),
|
||||
...renderThread(messagesData.messages, 0, forum.key)
|
||||
...renderThread(shownMsgs, 0, forum.key)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1103,12 +1103,19 @@ 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" })),
|
||||
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" }))
|
||||
);
|
||||
// topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS)
|
||||
const renderMobileTopbar = () => {
|
||||
const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "";
|
||||
return div(
|
||||
{ class: "oasis-mobile-topbar" },
|
||||
a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })),
|
||||
div({ class: "omt-menus" },
|
||||
a({ class: hiveFilter === "personal" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "personal" ? "/activity" : "/activity?hive=personal" }, span(i18n.menuPersonal || "Personal")),
|
||||
a({ class: hiveFilter === "community" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "community" ? "/activity" : "/activity?hive=community" }, span(i18n.menuCommunity || "Community"))
|
||||
),
|
||||
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 PINNABLE_MODULES = [
|
||||
{ key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null },
|
||||
|
|
@ -1170,26 +1177,18 @@ const renderBottomBar = () => {
|
|||
(badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "",
|
||||
span({ class: "bb-lbl" }, label)
|
||||
);
|
||||
const MAX = 4;
|
||||
const pins = (cfg.bottomBarPins || [])
|
||||
.map(key => PINNABLE_MODULES.find(m => m.key === key))
|
||||
.filter(m => m && modOn(m.mod))
|
||||
.slice(0, MAX);
|
||||
const slots = pins.map(m => bbLink(
|
||||
m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0
|
||||
));
|
||||
const editSlot = a(
|
||||
{ class: "bb-item bb-add", href: "/settings/bottombar" },
|
||||
span({ class: "bb-ico" }, pins.length < MAX ? "+" : "✎"),
|
||||
span({ class: "bb-lbl" }, pins.length < MAX ? (i18n.bbAdd || "Add") : (i18n.bbManage || "Edit"))
|
||||
);
|
||||
const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ];
|
||||
if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites"));
|
||||
// barra inferior FIJA (estilo epsylon): Inbox · PM · Write · Search · Graphos · Peers
|
||||
const items = [
|
||||
bbLink("/inbox", "☂", "Inbox", inboxN),
|
||||
bbLink("/pm", "✉", "PM"),
|
||||
bbLink("/publish", "✎", "Write"),
|
||||
bbLink("/search", "ꔅ", "Search"),
|
||||
bbLink("/graphos", "◈", "Graphos"),
|
||||
bbLink("/peers", "⧖", "Peers"),
|
||||
];
|
||||
return nav(
|
||||
{ class: "oasis-bottombar", "aria-label": i18n.bbQuickActions || "Quick actions" },
|
||||
...slots,
|
||||
editSlot,
|
||||
div({ class: "bb-pair" }, ...pair)
|
||||
{ class: "oasis-bottombar oasis-bottombar-fixed", "aria-label": i18n.bbQuickActions || "Quick actions" },
|
||||
...items
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -1292,33 +1291,8 @@ const template = (titlePrefix, ...elements) => {
|
|||
div(
|
||||
{ class: uxMode === "ainav" ? "header ainav-only" : "header" },
|
||||
renderMobileTopbar(),
|
||||
div(
|
||||
{ class: "oasis-mobile-menus" },
|
||||
// enlaces (no checkboxes): funcionan en CUALQUIER página → navegan al home /activity con el filtro.
|
||||
// toggle: si el filtro ya está activo, el enlace lleva a /activity (se deselecciona → todos)
|
||||
a({ class: hiveFilter === "personal" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "personal" ? "/activity" : "/activity?hive=personal" }, span(i18n.menuPersonal || "Personal")),
|
||||
a({ class: hiveFilter === "community" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "community" ? "/activity" : "/activity?hive=community" }, span(i18n.menuCommunity || "Community"))
|
||||
),
|
||||
(() => {
|
||||
const aiNavOn = getConfig().modules.aiNavMod === 'on';
|
||||
if (!aiNavOn && uxMode !== 'ainav') return null;
|
||||
return div(
|
||||
{ class: uxMode === 'ainav' ? "top-bar-center top-bar-center-ainav" : "top-bar-center" },
|
||||
form(
|
||||
{ method: 'POST', action: '/ai/ask', class: 'ai-ask-form' },
|
||||
input({
|
||||
type: 'text',
|
||||
name: 'q',
|
||||
class: 'ai-ask-input',
|
||||
placeholder: i18n.aiNavPlaceholder || 'Where do you want to go?',
|
||||
autocomplete: 'off',
|
||||
maxlength: '300',
|
||||
autofocus: uxMode === 'ainav' ? 'autofocus' : undefined
|
||||
}),
|
||||
button({ type: 'submit', class: 'ai-ask-btn' }, '➤')
|
||||
)
|
||||
);
|
||||
})()
|
||||
// hive global: los hexágonos salen en TODAS las páginas y SCROLLEAN bajo la topbar fija (antes solo en /activity)
|
||||
renderHiveNav()
|
||||
),
|
||||
(() => {
|
||||
const updateFlagPath = path.join(__dirname, '../server/.update_required');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue