[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:
s1to 2026-08-05 23:36:18 +02:00
parent 2b2c65fc7a
commit ed9f01e8e6
8 changed files with 280 additions and 129 deletions

View file

@ -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) => {