[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

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