[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
|
|
@ -3832,7 +3832,7 @@ router
|
|||
try { const oi = await forumModel.getOpenInvite(forumId).catch(() => null); if (oi && forumObj) forumObj.openInviteCode = oi.code; } catch (_) {}
|
||||
const forumMsgs = await forumModel.getMessagesByForumId(forumId);
|
||||
await warmAuthorNames(forumObj, forumMsgs);
|
||||
ctx.body = await singleForumView(forumObj, forumMsgs, ctx.query.filter, isReply ? ctx.params.forumId : null, { spreads: spreadInfo });
|
||||
ctx.body = await singleForumView(forumObj, forumMsgs, ctx.query.filter, ctx.query.topic || '');
|
||||
})
|
||||
.get('/welcome', async (ctx) => {
|
||||
const lang = ctx.cookies.get('language') || getConfig().language || 'en';
|
||||
|
|
@ -4601,7 +4601,7 @@ router
|
|||
.get("/chats/:chatId", async (ctx) => {
|
||||
if (!checkMod(ctx, 'chatsMod')) { ctx.redirect('/modules'); return; }
|
||||
await chatsModel.ingestKeys().catch(() => {});
|
||||
const { filter = 'all', q = '' } = ctx.query;
|
||||
const { filter = 'all', q = '', replyTo = '' } = ctx.query;
|
||||
const uid = getViewerId();
|
||||
let chat = await chatsModel.getChatById(ctx.params.chatId);
|
||||
if (!chat) { ctx.redirect('/chats'); return; }
|
||||
|
|
@ -4621,7 +4621,8 @@ router
|
|||
const fav = await mediaFavorites.getFavoriteSet('chats');
|
||||
const messages = await chatsModel.listMessages(chat.rootId || chat.key);
|
||||
const isTribeMember = !!parentTribe;
|
||||
ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']) });
|
||||
const chatListForSwitcher = process.env.OASIS_MOBILE === '1' ? await chatsModel.listAll({ viewerId: uid }).catch(() => []) : [];
|
||||
ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']), replyTo: typeof replyTo === 'string' ? replyTo : '', chatList: chatListForSwitcher });
|
||||
})
|
||||
.get("/pads", async (ctx) => {
|
||||
if (!checkMod(ctx, 'padsMod')) { ctx.redirect('/modules'); return; }
|
||||
|
|
@ -6116,8 +6117,9 @@ router
|
|||
.post('/forum/:id/message', koaBody(), async ctx => {
|
||||
const { message, parentId } = ctx.request.body;
|
||||
const cleanedMsg = stripDangerousTags(message);
|
||||
const subtopic = stripDangerousTags(String(ctx.request.body.subtopic || '').trim()).slice(0, 40);
|
||||
const mentions = await extractMentions(cleanedMsg);
|
||||
await forumModel.addMessageToForum(ctx.params.id, { text: cleanedMsg, author: getViewerId(), timestamp: new Date().toISOString(), mentions: mentions.length > 0 ? mentions : undefined }, parentId);
|
||||
await forumModel.addMessageToForum(ctx.params.id, { text: cleanedMsg, author: getViewerId(), timestamp: new Date().toISOString(), mentions: mentions.length > 0 ? mentions : undefined, subtopic: subtopic || undefined }, parentId);
|
||||
ctx.redirect(`/forum/${encodeURIComponent(ctx.params.id)}`);
|
||||
})
|
||||
.post('/forum/:forumId/vote', koaBody(), async ctx => {
|
||||
|
|
@ -7500,8 +7502,10 @@ router
|
|||
}
|
||||
const text = stripDangerousTags(String(ctx.request.body.text || '').trim());
|
||||
const imageBlob = ctx.request.files?.image ? extractBlobId(await handleBlobUpload(ctx, 'image')) : null;
|
||||
const replyToRaw = String(ctx.request.body.replyTo || '').trim();
|
||||
const replyTo = replyToRaw.startsWith('%') ? replyToRaw : null;
|
||||
if (!text && !imageBlob) { ctx.redirect(`/chats/${encodeURIComponent(ctx.params.chatId)}`); return; }
|
||||
await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob);
|
||||
await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob, replyTo);
|
||||
ctx.redirect(safeReturnTo(ctx, `/chats/${encodeURIComponent(ctx.params.chatId)}`, ['/chats']));
|
||||
})
|
||||
.post("/pads/create", koaBody(), async (ctx) => {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ body { overflow-x: clip; overflow-y: visible; }
|
|||
padding: 2px 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.oasis-bottombar .bb-ico { font-size: 18px; line-height: 1; color: #FFA500; }
|
||||
.oasis-bottombar .bb-lbl { font-size: 9px; color: #ccc; white-space: nowrap; }
|
||||
.oasis-bottombar .bb-ico { font-size: 18px; line-height: 1; color: #FFD700; }
|
||||
.oasis-bottombar .bb-lbl { font-size: 9px; color: #FFD700; white-space: nowrap; }
|
||||
.oasis-bottombar .bb-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
|
|
@ -34,7 +34,7 @@ body { overflow-x: clip; overflow-y: visible; }
|
|||
min-width: 15px; height: 15px;
|
||||
padding: 0 3px;
|
||||
border-radius: 8px;
|
||||
background: #FFA500;
|
||||
background: #FFD700;
|
||||
color: #121212;
|
||||
font-size: 9px; font-weight: bold;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
|
|
@ -42,33 +42,51 @@ body { overflow-x: clip; overflow-y: visible; }
|
|||
|
||||
body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
|
||||
|
||||
/* topbar FIJA arriba: [logo] [Personal] [Community] [avatar]; el hive scrollea por debajo */
|
||||
.oasis-mobile-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
box-shadow: none; border: 0;
|
||||
background: #121212;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
body { padding-top: 84px; background: #121212; }
|
||||
|
||||
.oasis-mobile-topbar .omt-logo, .oasis-mobile-topbar .omt-avatar { flex: 0 0 auto; }
|
||||
.oasis-mobile-topbar .omt-logo img,
|
||||
.oasis-mobile-topbar .omt-avatar img {
|
||||
width: 32px; height: 32px;
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #FFA500;
|
||||
border: 2px solid #FFD700;
|
||||
object-fit: cover; display: block;
|
||||
}
|
||||
|
||||
.oasis-mobile-topbar .omt-title {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
font-size: 26px; font-weight: bold;
|
||||
letter-spacing: 10px; text-indent: 10px;
|
||||
color: #FFD700; text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background: transparent; border: 0; box-shadow: none; padding: 0;
|
||||
.oasis-mobile-topbar .omt-menus {
|
||||
flex: 1 1 auto; min-width: 0;
|
||||
display: flex; justify-content: center; gap: 8px;
|
||||
}
|
||||
.oasis-mobile-topbar .omt-menus .oasis-menu-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid #FFD700; border-radius: 8px;
|
||||
background: #161616; color: #FFD700;
|
||||
font-weight: 600; letter-spacing: .5px; text-decoration: none;
|
||||
font-size: 13px; white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
.oasis-mobile-topbar .omt-menus .oasis-menu-btn.active {
|
||||
background: #FFD700; color: #121212; border-color: #FFD700;
|
||||
}
|
||||
.oasis-mobile-topbar .omt-menus .oasis-menu-btn span { text-decoration: none; }
|
||||
|
||||
/* ANULAR el "card" global de style.css `div{background:#222;border-radius:10px;box-shadow;padding:20px;margin-bottom:20px}`
|
||||
en nuestro chrome móvil propio: era el 2º color (caja mas clara) detras de los botones,
|
||||
la caja redondeada de la topbar y el fondo #222 detras de los hexagonos. */
|
||||
.header, .hive-nav, .hive-grid, .hive-cats, .hive-row,
|
||||
.oasis-mobile-topbar, .oasis-mobile-topbar .omt-menus {
|
||||
background-color: transparent; box-shadow: none; border-radius: 0;
|
||||
}
|
||||
.oasis-mobile-topbar { background-color: #121212; } /* fija: fondo opaco (el contenido scrollea por debajo) */
|
||||
.oasis-mobile-topbar .omt-menus { padding: 0; margin: 0; } /* sin caja ni relleno detras de los botones */
|
||||
|
||||
.header .top-bar-left > a.logo-icon { display: none; }
|
||||
|
||||
|
|
@ -77,14 +95,15 @@ body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
|
|||
.oasis-mobile-menus .oasis-menu-btn {
|
||||
flex: 1 1 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 12px;
|
||||
border: 1.5px solid #333; border-radius: 10px;
|
||||
background: #121212; color: #ccc;
|
||||
font-weight: bold; letter-spacing: 1px; cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #FFD700; border-radius: 8px;
|
||||
background: #161616; color: #FFD700;
|
||||
font-weight: 600; letter-spacing: 1px; text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.oasis-mobile-menus .oasis-menu-btn span { text-decoration: none; }
|
||||
|
||||
.oasis-mobile-menus .oasis-menu-btn.active {
|
||||
background: #FFA500; color: #121212; border-color: #FFA500;
|
||||
background: #FFD700; color: #121212; border-color: #FFD700;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
|
@ -98,12 +117,12 @@ body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
|
|||
width: 46px; height: 46px;
|
||||
margin-top: -22px;
|
||||
border-radius: 50%;
|
||||
background: #FFA500; color: #121212;
|
||||
background: #FFD700; color: #121212;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.4);
|
||||
}
|
||||
.oasis-bottombar .bb-fab .bb-lbl { color: #FFA500; font-weight: bold; }
|
||||
.oasis-bottombar .bb-fab .bb-lbl { color: #FFD700; font-weight: bold; }
|
||||
|
||||
.main-column, .main-column p, .main-column li, .main-column a,
|
||||
.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 {
|
||||
|
|
@ -144,13 +163,13 @@ body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
|
|||
.oasis-nav-list li a {
|
||||
display: flex; align-items: center; gap: var(--space-3);
|
||||
min-height: 48px; padding: 0 var(--space-3);
|
||||
color: #e8e8e8; text-decoration: none;
|
||||
color: #FFD700; text-decoration: none;
|
||||
border-top: 1px solid #262626;
|
||||
}
|
||||
.oasis-nav-list li a .emoji { flex: 0 0 auto; width: 22px; text-align: center; color: #FFA500; }
|
||||
.oasis-nav-list li a .emoji { flex: 0 0 auto; width: 22px; text-align: center; color: #FFD700; }
|
||||
.oasis-nav-list li a .nav-text { font-size: var(--fs-body); }
|
||||
.oasis-nav-list li a:active { background: #262626; }
|
||||
.oasis-nav-list li a.current { color: #FFA500; font-weight: bold; }
|
||||
.oasis-nav-list li a.current { color: #FFD700; font-weight: bold; }
|
||||
|
||||
.main-column h1 { font-size: var(--fs-title); font-weight: 700; line-height: 1.25; margin: 0 0 var(--space-2); }
|
||||
.main-column h2 { font-size: 18px; font-weight: 700; line-height: 1.3; margin: 0 0 var(--space-2); }
|
||||
|
|
@ -198,11 +217,11 @@ body { overscroll-behavior: none; }
|
|||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
width: 100%; min-height: 44px; margin: 4px 0 8px; padding: 10px 12px;
|
||||
border: 1.5px solid #333; border-radius: 10px;
|
||||
background: #121212; color: #ccc;
|
||||
background: #121212; color: #FFD700;
|
||||
font-weight: bold; letter-spacing: 1px; text-transform: uppercase; cursor: pointer;
|
||||
}
|
||||
.activity-filter-caret { transition: transform 150ms ease; }
|
||||
#activity-filter-toggle:checked ~ .activity-filter-btn { background: #FFA500; color: #121212; border-color: #FFA500; }
|
||||
#activity-filter-toggle:checked ~ .activity-filter-btn { background: #FFD700; color: #121212; border-color: #FFD700; }
|
||||
#activity-filter-toggle:checked ~ .activity-filter-btn .activity-filter-caret { transform: rotate(180deg); }
|
||||
.activity-filter-panel { display: none; }
|
||||
#activity-filter-toggle:checked ~ .activity-filter-panel { display: block; }
|
||||
|
|
@ -213,7 +232,7 @@ body { overscroll-behavior: none; }
|
|||
.bb-manage { margin: 8px 0 16px; }
|
||||
.bb-manage h3, .bb-manage-add h3 {
|
||||
font-size: 13px; text-transform: uppercase; letter-spacing: .05em;
|
||||
color: #FFA500; margin: 14px 0 8px;
|
||||
color: #FFD700; margin: 14px 0 8px;
|
||||
}
|
||||
.bb-manage-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.bb-manage-form { margin: 0; width: 100%; }
|
||||
|
|
@ -222,11 +241,11 @@ body { overscroll-behavior: none; }
|
|||
display: flex; align-items: center; gap: 12px;
|
||||
min-height: 52px; padding: 0 14px;
|
||||
background: #1c1c1c; border: 1px solid #333; border-radius: 10px;
|
||||
color: #eee; cursor: pointer; text-align: left;
|
||||
color: #FFD700; cursor: pointer; text-align: left;
|
||||
}
|
||||
.bb-manage-ico { font-size: 20px; color: #FFA500; width: 26px; text-align: center; }
|
||||
.bb-manage-ico { font-size: 20px; color: #FFD700; width: 26px; text-align: center; }
|
||||
.bb-manage-lbl { flex: 1 1 auto; font-size: 15px; }
|
||||
.bb-manage-x { font-size: 16px; color: #ff6b6b; font-weight: bold; }
|
||||
.bb-manage-x { font-size: 16px; color: #FFD700; font-weight: bold; }
|
||||
.bb-manage-empty, .bb-manage-full { color: #999; font-size: 14px; padding: 8px 2px; }
|
||||
.bb-manage-done {
|
||||
display: block; text-align: center;
|
||||
|
|
@ -244,7 +263,7 @@ body { overscroll-behavior: none; }
|
|||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
|
||||
text-decoration: none; cursor: pointer; box-sizing: border-box;
|
||||
background: #D9A400;
|
||||
background: #FFD700;
|
||||
filter: drop-shadow(0 1px 2px rgba(0,0,0,.6));
|
||||
}
|
||||
.hex-cat::before, .hex-mod::before {
|
||||
|
|
@ -255,10 +274,10 @@ body { overscroll-behavior: none; }
|
|||
}
|
||||
.hex-cat { width: var(--hx); height: calc(var(--hx) * 1.15); }
|
||||
.hex-mod { width: var(--hxm); height: calc(var(--hxm) * 1.15); }
|
||||
.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFA500; line-height: 1; position: relative; z-index: 1; }
|
||||
.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFD700; line-height: 1; position: relative; z-index: 1; }
|
||||
.hex-cat .hx-ico { font-size: 17px; }
|
||||
.hex-mod .hx-ico { font-size: 16px; }
|
||||
.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #d8d8d8; text-align: center; line-height: 1; font-weight: bold; overflow: hidden; position: relative; z-index: 1; }
|
||||
.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #FFD700; text-align: center; line-height: 1; font-weight: bold; overflow: hidden; position: relative; z-index: 1; }
|
||||
.hex-cat .hx-lbl { font-size: 8px; max-width: 88%; }
|
||||
.hex-mod .hx-lbl { font-size: 8px; max-width: 92%; }
|
||||
.hex-cat:active::before, .hex-mod:active::before { background: #242424; }
|
||||
|
|
@ -271,7 +290,7 @@ body { overscroll-behavior: none; }
|
|||
#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"],
|
||||
#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"],
|
||||
#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"],
|
||||
#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFA500; }
|
||||
#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFD700; }
|
||||
#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"]::before,
|
||||
#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"]::before,
|
||||
#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"]::before,
|
||||
|
|
@ -281,7 +300,7 @@ body { overscroll-behavior: none; }
|
|||
#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"]::before,
|
||||
#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"]::before,
|
||||
#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"]::before,
|
||||
#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFA500; }
|
||||
#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFD700; }
|
||||
#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"] :is(.hx-ico,.hx-lbl),
|
||||
#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"] :is(.hx-ico,.hx-lbl),
|
||||
#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"] :is(.hx-ico,.hx-lbl),
|
||||
|
|
@ -322,11 +341,11 @@ body { overscroll-behavior: none; }
|
|||
.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px; }
|
||||
|
||||
.oasis-bottombar a.bb-item.current .bb-ico {
|
||||
background: #FFA500; color: #121212;
|
||||
background: #FFD700; color: #121212;
|
||||
border-radius: 999px; padding: 4px 12px;
|
||||
}
|
||||
.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFA500; font-weight: bold; }
|
||||
.oasis-bottombar a.bb-item.current .bb-badge { background: #121212; color: #FFA500; }
|
||||
.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFD700; font-weight: bold; }
|
||||
.oasis-bottombar a.bb-item.current .bb-badge { background: #121212; color: #FFD700; }
|
||||
|
||||
.main-column, .main-column p, .main-column li, .main-column a,
|
||||
.main-column span, .main-column td, .main-column th, .main-column label,
|
||||
|
|
@ -402,7 +421,7 @@ button, input[type="submit"], input[type="button"],
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(255,165,0,0.18);
|
||||
border-bottom: 1px solid rgba(255,215,0,0.18);
|
||||
border-radius: 0;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 6px 0;
|
||||
|
|
@ -515,7 +534,7 @@ button, input[type="submit"], input[type="button"],
|
|||
.activity-groups .act-count {
|
||||
flex: 0 0 auto; min-width: 20px; text-align: center;
|
||||
font-size: 11px; font-weight: 700; color: #121212;
|
||||
background: #FFA500; border-radius: 999px; padding: 1px 7px;
|
||||
background: #FFD700; border-radius: 999px; padding: 1px 7px;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +545,7 @@ button, input[type="submit"], input[type="button"],
|
|||
min-height: 44px; margin: 2px 0; box-sizing: border-box;
|
||||
}
|
||||
.activity-groups .oasis-nav-list .filter-btn.active {
|
||||
outline: 2px solid #FFA500; outline-offset: -2px;
|
||||
outline: 2px solid #FFD700; outline-offset: -2px;
|
||||
}
|
||||
|
||||
.message-list { display: flex; flex-direction: column; gap: 4px; width: 100%; }
|
||||
|
|
@ -550,10 +569,10 @@ button, input[type="submit"], input[type="button"],
|
|||
border-bottom-right-radius: 4px; background: #2a2413; border-color: #5c4c1f;
|
||||
}
|
||||
|
||||
.pm-bubble .pm-bubble-sender { font-size: 11px; font-weight: 700; color: #FFA500; margin-bottom: 3px; }
|
||||
.pm-bubble .pm-bubble-sender a { color: #FFA500; }
|
||||
.pm-bubble .pm-bubble-sender { font-size: 11px; font-weight: 700; color: #FFD700; margin-bottom: 3px; }
|
||||
.pm-bubble .pm-bubble-sender a { color: #FFD700; }
|
||||
|
||||
.pm-bubble .message-text { font-size: 15px; line-height: 1.42; color: #ececec; margin: 0; }
|
||||
.pm-bubble .message-text { font-size: 15px; line-height: 1.42; color: #FFD700; margin: 0; }
|
||||
.pm-bubble .message-text a { color: #FFD700; }
|
||||
|
||||
.pm-bubble .pm-bubble-meta {
|
||||
|
|
@ -737,7 +756,7 @@ button, input[type="submit"], input[type="button"],
|
|||
padding: 40px 16px; gap: 8px; width: 100%; box-sizing: border-box;
|
||||
}
|
||||
.empty-state-icon { font-size: 44px; line-height: 1; opacity: 0.55; }
|
||||
.empty-state-title { font-size: 16px; font-weight: 700; margin: 4px 0 0 0; color: #eee; }
|
||||
.empty-state-title { font-size: 16px; font-weight: 700; margin: 4px 0 0 0; color: #FFD700; }
|
||||
.empty-state-text { color: var(--c-muted, #8c8c8c); font-size: 14px; line-height: 1.4; max-width: 40ch; margin: 0; }
|
||||
.empty-state-cta { margin-top: 10px; min-height: 48px; display: inline-flex; align-items: center; }
|
||||
|
||||
|
|
@ -751,7 +770,7 @@ button, input[type="submit"], input[type="button"],
|
|||
}
|
||||
.activity-back-arrow { font-size: 22px; line-height: 1; font-weight: bold; }
|
||||
.activity-active-filter {
|
||||
font-weight: 700; color: #FFA500; text-transform: uppercase;
|
||||
font-weight: 700; color: #FFD700; text-transform: uppercase;
|
||||
font-size: 13px; letter-spacing: .03em;
|
||||
}
|
||||
|
||||
|
|
@ -766,7 +785,7 @@ button, input[type="submit"], input[type="button"],
|
|||
.score-btn:active, .vote-btn:active, .feed-action-btn:active { filter: brightness(.85); }
|
||||
|
||||
.oasis-bottombar .bb-add .bb-ico {
|
||||
background: #FFA500; color: #121212; border-radius: 999px;
|
||||
background: #FFD700; color: #121212; border-radius: 999px;
|
||||
width: 30px; height: 30px; display: inline-flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-weight: bold; font-size: 20px; line-height: 1;
|
||||
|
|
@ -780,10 +799,10 @@ button, input[type="submit"], input[type="button"],
|
|||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; gap: 6px; width: 100%;
|
||||
min-height: 76px; padding: 10px 6px; box-sizing: border-box;
|
||||
background: #1c1c1c; border: 1px solid #333; border-radius: 12px; color: #eee;
|
||||
background: #1c1c1c; border: 1px solid #333; border-radius: 12px; color: #FFD700;
|
||||
}
|
||||
.bb-pick-btn.active { border-color: #FFA500; background: #2a2412; }
|
||||
.bb-pick-ico { font-size: 26px; color: #FFA500; line-height: 1; }
|
||||
.bb-pick-btn.active { border-color: #FFD700; background: #2a2412; }
|
||||
.bb-pick-ico { font-size: 26px; color: #FFD700; line-height: 1; }
|
||||
.bb-pick-lbl { font-size: 11px; text-align: center; line-height: 1.2; }
|
||||
|
||||
|
||||
|
|
@ -818,10 +837,10 @@ button, input[type="submit"], input[type="button"],
|
|||
}
|
||||
.chat-message-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 3px; font-size: 10px; color: var(--c-muted, #8c8c8c); }
|
||||
.chat-message.chat-message-self .chat-message-meta { justify-content: flex-end; }
|
||||
.chat-message-sender, .chat-message-sender a { font-size: 11px; font-weight: 700; color: #FFA500; }
|
||||
.chat-message-sender, .chat-message-sender a { font-size: 11px; font-weight: 700; color: #FFD700; }
|
||||
.chat-message-date { opacity: .8; }
|
||||
.chat-message-text {
|
||||
display: block; font-size: 15px; line-height: 1.42; color: #ececec; margin: 0;
|
||||
display: block; font-size: 15px; line-height: 1.42; color: #FFD700; margin: 0;
|
||||
word-break: break-word; overflow-wrap: anywhere;
|
||||
}
|
||||
.chat-message-text a { color: #FFD700; }
|
||||
|
|
@ -842,6 +861,85 @@ button, input[type="submit"], input[type="button"],
|
|||
.chat-message-form .filter-btn::before { content: "\27A4"; font-size: 18px; line-height: 1; color: #121212; }
|
||||
.chat-message-form input[type="file"] { font-size: 12px; }
|
||||
|
||||
/* --- Reply a mensaje en chats (server-side ?replyTo=, sin JS) --- */
|
||||
.chat-message-meta .chat-reply-btn { margin-left: auto; padding: 0 4px; color: #FFD700; text-decoration: none; font-size: 15px; opacity: .85; }
|
||||
.chat-message-self .chat-message-meta .chat-reply-btn { margin-left: 8px; }
|
||||
.chat-quote {
|
||||
display: block; border-left: 3px solid #FFD700; padding: 3px 8px; margin: 0 0 5px;
|
||||
background: rgba(255,215,0,.08); border-radius: 6px; font-size: 12px; line-height: 1.3;
|
||||
}
|
||||
.chat-quote-author, .chat-quote-author a { color: #FFD700; font-weight: 700; margin-right: 6px; }
|
||||
.chat-quote-text { color: #FFDD44; }
|
||||
.chat-replying-to {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
||||
border-left: 3px solid #FFD700; background: rgba(255,215,0,.10);
|
||||
padding: 6px 10px; margin-bottom: 8px; border-radius: 8px; font-size: 12px;
|
||||
}
|
||||
.chat-replying-label { color: #FFD700; font-weight: 700; }
|
||||
.chat-reply-cancel { margin-left: auto; color: #FFD700; text-decoration: none; font-weight: 700; font-size: 14px; }
|
||||
|
||||
/* --- Switcher de chats: tira horizontal para saltar entre conversaciones --- */
|
||||
.chat-switcher {
|
||||
display: flex; gap: 6px; overflow-x: auto; padding: 6px 0; margin: 4px 0 8px;
|
||||
-webkit-overflow-scrolling: touch; scrollbar-width: none;
|
||||
}
|
||||
.chat-switcher::-webkit-scrollbar { display: none; }
|
||||
.chat-switch-chip {
|
||||
flex: 0 0 auto; max-width: 140px; padding: 6px 12px; border-radius: 999px;
|
||||
background: #1c1c1c; border: 1px solid #333; color: #FFD700; text-decoration: none;
|
||||
font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.chat-switch-chip.active { background: #FFD700; color: #121212; border-color: #FFD700; font-weight: 700; }
|
||||
|
||||
/* --- Subtopics de foro: tira de pestañas (topics con nombre) + input de topic en el compose --- */
|
||||
.forum-topics {
|
||||
display: flex; gap: 6px; overflow-x: auto; padding: 6px 0; margin: 4px 0 8px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.forum-topics::-webkit-scrollbar { display: none; }
|
||||
.forum-topic-chip {
|
||||
flex: 0 0 auto; max-width: 160px; padding: 6px 14px; border-radius: 999px;
|
||||
background: #1c1c1c; border: 1px solid #333; color: #FFD700; text-decoration: none;
|
||||
font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.forum-topic-chip.active { background: #FFD700; color: #121212; border-color: #FFD700; font-weight: 700; }
|
||||
.new-message-form { flex-wrap: wrap; }
|
||||
.forum-topic-input {
|
||||
flex: 1 0 100%; box-sizing: border-box; border-radius: 12px;
|
||||
padding: 8px 12px; font-size: 13px; margin: 0 0 6px;
|
||||
}
|
||||
|
||||
/* --- #3: al desplegar una categoría, sus módulos son BOTONES CUADRADOS redondeados (no hexágonos),
|
||||
en rejilla 2×2 (2 y 2) y grandes: cada cuadrado llena su columna --- */
|
||||
.hive-mods {
|
||||
display: grid; grid-template-columns: repeat(2, 1fr);
|
||||
justify-items: stretch; align-content: center;
|
||||
gap: 12px; padding: 8px 4px;
|
||||
}
|
||||
.hex-mod {
|
||||
clip-path: none; -webkit-clip-path: none;
|
||||
width: 100%; height: auto; aspect-ratio: 1 / 1; min-height: 120px;
|
||||
border-radius: 18px; padding: 8px; gap: 7px;
|
||||
}
|
||||
.hex-mod::before { clip-path: none; -webkit-clip-path: none; inset: 2px; border-radius: 16px; }
|
||||
.hex-mod .hx-ico { font-size: 36px; }
|
||||
.hex-mod .hx-lbl { font-size: 12.5px; max-width: 94%; }
|
||||
/* si el nº de módulos es IMPAR, el último (solo en su fila) se CENTRA en vez de pegarse a la izquierda;
|
||||
abarca las 2 columnas pero conserva el ancho de una sola (gap 12 → mitad 6) */
|
||||
.hex-mod:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1; justify-self: center; width: calc(50% - 6px);
|
||||
}
|
||||
|
||||
/* --- #2: el hexágono "Personal" muestra tu avatar (diferencia del filtro "Personal" del header) --- */
|
||||
.hex-cat .hx-avatar {
|
||||
width: 58%; height: 58%; border-radius: 50%; object-fit: cover;
|
||||
position: relative; z-index: 1; border: 1px solid rgba(255,215,0,.6);
|
||||
}
|
||||
|
||||
/* --- #1: chat estilo Signal — compose ABAJO + mensajes arriba (el reply deja de "saltar") --- */
|
||||
.tribe-main.chat-full-width { display: flex; flex-direction: column-reverse; }
|
||||
#chatbox { scroll-margin-top: 8px; scroll-margin-bottom: 8px; }
|
||||
|
||||
/* --- Botón spread → pill compacto (vence button{min-height:48px}/ancho completo) --- */
|
||||
.card-spread-centered, .card-spread-left, .spread-row { margin: 6px 0; }
|
||||
.card-spread-centered .spread-btn, .card-spread-left .spread-btn,
|
||||
|
|
@ -849,10 +947,10 @@ button, input[type="submit"], input[type="button"],
|
|||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: auto; min-width: 0; min-height: 30px; height: 30px;
|
||||
padding: 2px 14px; font-size: 13px; line-height: 1;
|
||||
border-radius: 999px; border: 1px solid #555; background: #1a1a1a; color: #ffa500;
|
||||
border-radius: 999px; border: 1px solid #555; background: #1a1a1a; color: #FFD700;
|
||||
}
|
||||
.card-spread-centered .spread-btn.spread-btn-on,
|
||||
.card-spread-left .spread-btn.spread-btn-on { background: #ffa500; color: #1a1a1a; border-color: #ff7300; }
|
||||
.card-spread-left .spread-btn.spread-btn-on { background: #FFD700; color: #1a1a1a; border-color: #FFD700; }
|
||||
|
||||
/* --- Caja de votos (▲ n ▼): compacta y HORIZONTAL.
|
||||
El problema era una cadena de flex-stretch (comment-vote-col flex → estiraba la caja a 135px).
|
||||
|
|
@ -920,9 +1018,29 @@ button, input[type="submit"], input[type="button"],
|
|||
.forum-comment.level-3 { margin-left: 28px !important; border-left: 2px solid #404040; }
|
||||
.forum-comment.level-4 { margin-left: 34px !important; border-left: 2px solid #454545; }
|
||||
/* comentario destacado: acento sutil a la izquierda en vez del marco naranja enorme */
|
||||
.forum-comment.highlighted-reply { border: 1px solid #3a3a3a !important; border-left: 3px solid #FFA500 !important; background: #1b1a16 !important; }
|
||||
.forum-comment.highlighted-reply { border: 1px solid #3a3a3a !important; border-left: 3px solid #FFD700 !important; background: #1b1a16 !important; }
|
||||
|
||||
/* --- Reply del foro SIN tocar la vista de epsylon: la cajita se agranda al enfocarla;
|
||||
el botón redondo ↩ está SIEMPRE visible (el usuario pidió icono de reply, no gesto/JS). --- */
|
||||
.new-reply .comment-textarea { min-height: 42px; }
|
||||
.new-reply:focus-within .comment-textarea { min-height: 64px; }
|
||||
|
||||
/* ==========================================================================
|
||||
Mejoras UI 0.9.1: fuente única + barra de búsqueda en topbar + hive global + bottombar fija
|
||||
========================================================================== */
|
||||
|
||||
/* --- fuente única en todo el UI móvil (quitar el lío monospace/sans) --- */
|
||||
.oasis-mobile-topbar, .oasis-mobile-menus, .hive-nav, .oasis-bottombar, .main-column,
|
||||
.oasis-mobile-topbar input, .main-column input, .main-column textarea, .main-column select, button {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* (topbar sin barra de búsqueda: vuelve el título "OASIS" — CSS .omt-title más arriba) */
|
||||
|
||||
/* --- hive ahora global en el header (debajo de los 2 botones) --- */
|
||||
.header .hive-nav { margin: 34px auto 10px; }
|
||||
|
||||
/* --- bottombar FIJA de 6 (Inbox·PM·Write·Search·Graphos·Peers): que quepan --- */
|
||||
.oasis-bottombar-fixed .bb-item { padding: 2px 0; gap: 2px; }
|
||||
.oasis-bottombar-fixed .bb-ico { font-size: 17px; }
|
||||
.oasis-bottombar-fixed .bb-lbl { font-size: 8.5px; }
|
||||
|
|
|
|||
|
|
@ -293,7 +293,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
|||
text,
|
||||
image: c.image || null,
|
||||
author: c.author || node.author,
|
||||
createdAt: c.createdAt || new Date(node.ts).toISOString()
|
||||
createdAt: c.createdAt || new Date(node.ts).toISOString(),
|
||||
replyTo: c.replyTo || null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -791,7 +792,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
|||
} catch (_) { return 0 }
|
||||
},
|
||||
|
||||
async sendMessage(chatId, text, image = null) {
|
||||
async sendMessage(chatId, text, image = null, replyTo = null) {
|
||||
const ssbClient = await openSsb()
|
||||
const userId = ssbClient.id
|
||||
const chat = await this.getChatById(chatId)
|
||||
|
|
@ -808,7 +809,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
|||
const c = m.value?.content
|
||||
return c?.type === "chatMessage" && c?.chatId === chat.rootId && m.value?.author === userId && (m.value?.timestamp || 0) >= oneHourAgo
|
||||
}).length
|
||||
if (recentCount >= 3) throw new Error("Rate limit: max 3 messages per hour")
|
||||
if (process.env.OASIS_MOBILE !== '1' && recentCount >= 3) throw new Error("Rate limit: max 3 messages per hour")
|
||||
|
||||
const now = new Date().toISOString()
|
||||
let content = {
|
||||
|
|
@ -818,6 +819,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => {
|
|||
createdAt: now
|
||||
}
|
||||
if (image) content.image = image
|
||||
if (replyTo && typeof replyTo === "string" && replyTo.startsWith("%")) content.replyTo = replyTo
|
||||
|
||||
const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId)
|
||||
if (chatIsEncrypted && tribeCrypto) {
|
||||
|
|
|
|||
|
|
@ -503,7 +503,8 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => {
|
|||
text: c.text,
|
||||
author: c.author,
|
||||
timestamp: m.value.timestamp,
|
||||
parent: c.branch || null
|
||||
parent: c.branch || null,
|
||||
subtopic: c.subtopic || null
|
||||
}));
|
||||
for (let r of replies) {
|
||||
const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key);
|
||||
|
|
|
|||
|
|
@ -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