From 6ac51fcbe5b4e0ba375ef94e5e542b5e0617e33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 10 Jul 2026 13:33:47 +0200 Subject: [PATCH 1/7] docs(spec): room info modal to view full room topic (#922) --- .../2026-07-10-room-full-topic-view-design.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-10-room-full-topic-view-design.md diff --git a/docs/superpowers/specs/2026-07-10-room-full-topic-view-design.md b/docs/superpowers/specs/2026-07-10-room-full-topic-view-design.md new file mode 100644 index 00000000..a742e816 --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-room-full-topic-view-design.md @@ -0,0 +1,94 @@ +# Room Info modal — view a room's full topic/description (#922) + +**Issue:** [#922](https://github.com/processone/fluux-messenger/issues/922) — "No way to view a room's full description when it is too long." Milestone 0.17.1. + +## Problem + +A MUC room's description (`muc#roomconfig_roomdesc`) is surfaced in the app as `room.subject`. It renders in only one place — the header topic line in `RoomHeader.tsx` — where it is hard-clipped to a single line via Tailwind `truncate`: + +```jsx +// apps/fluux/src/components/RoomHeader.tsx:125-130 +
+

{room.name}

+

+ {room.subject ? renderTextWithLinks(room.subject) : room.jid} +

+
+``` + +When the topic exceeds one line, the rest is unreachable — there is no room-info surface that shows it in full. The only existing room modal is the owner-only `RoomConfigModal` (edit/config), which is not a viewing surface for regular members. + +## Goal + +Give any room member a way to read the complete topic/description, plus a lightweight quick-peek in the header. Keep the change minimal and consistent with existing patterns. + +## Non-goals + +- No SDK changes. `room.subject` already carries the text. +- No new i18n keys — `rooms.topic`, `chat.showMore`, `chat.showLess` already exist. +- Not adding the topic to the members panel (`OccupantPanel`) — it reads as out of place there. +- Not building a general-purpose room-details surface. The modal is scoped to identity + topic; it can grow later. + +## Design + +Two complementary changes. + +### 1. New `RoomInfoModal` (dedicated viewing area) + +New component `apps/fluux/src/components/RoomInfoModal.tsx`, built on the standard `ModalShell` primitive (`title`, `onClose`, `width?`, `children`; provides glass panel, scrim, Escape, focus restore). + +Props: +```ts +interface RoomInfoModalProps { + room: Room + onClose: () => void +} +``` + +Content (top to bottom): +- **Identity row**: `RoomAvatar` (existing component) + room `name`. +- **JID**: `room.jid`, shown in muted text with a copy affordance consistent with how JIDs are copied elsewhere (e.g. the occupant "copy JID" toast pattern). Copy is a nice-to-have; if it adds friction, render as selectable text only. +- **Topic block**: label `t('rooms.topic')` ("Topic"), then the full subject rendered with `renderTextWithLinks(room.subject)` inside a container with `whitespace-pre-wrap break-words` so it wraps fully and links stay clickable. When `room.subject` is empty, show a muted `t('rooms.noTopic')`-style placeholder — **verify the key exists**; if not, fall back to an existing empty-state key or add one following the i18n-surgical-edit workflow (all 33 locales, no English placeholders). + +**Long-topic collapse (self-contained, no message-list coupling):** +The topic body collapses past ~6 lines using CSS `line-clamp-6` with a local `useState` `expanded` flag and a **Show more / Show less** toggle (`chat.showMore` / `chat.showLess` + `ChevronDown`/`ChevronUp`). The toggle renders only when the content actually overflows, detected by a measure-on-mount + on-resize check (compare `scrollHeight` to `clientHeight` on the clamped element via a ref). This deliberately does **not** reuse `CollapsibleContent`, which requires a `messageId`, `expandedMessagesStore`, and the `messageWidthContext` provider — none of which exist outside the message list. The collapse logic here is ~40 lines and independently testable. + +### 2. `RoomHeader` — open the modal + quick-peek tooltip + +In `RoomHeader.tsx`: +- Add modal state: `const [showInfoModal, setShowInfoModal] = useState(false)` (matching the existing `showAvatarModal` / `showMembersModal` / `showHatsModal` pattern). +- Make the name+subject block (lines 125-130) a keyboard-accessible **button** (`role="button"`, focusable, `Enter`/`Space`) that calls `setShowInfoModal(true)`. Add a subtle hover affordance (e.g. `hover:bg-fluux-hover` rounded) so the click target is discoverable. Preserve the desktop drag region — the title button must stop the drag interaction from swallowing the click (opt the button out of the drag region as other interactive header controls do). +- Wrap the block (or the subject `

`) in the existing `Tooltip` with `content={room.subject}` and `position="bottom"`, rendered only when `room.subject` is present, for an instant hover/long-press peek. +- Render `{showInfoModal && setShowInfoModal(false)} />}` alongside the other conditionally-rendered modals near the end of the component. + +## Data flow + +`room` (already available in `RoomHeader`) → `RoomInfoModal` via props. No store or SDK reads beyond the existing `Room` object. The topic value is `room.subject`. + +## Error / edge cases + +- **No subject**: header shows `room.jid` (unchanged); modal shows the empty-topic placeholder. Header title still opens the modal (useful for JID/avatar). +- **Very long / multi-paragraph subject**: handled by `whitespace-pre-wrap` + line-clamp collapse. +- **Links in subject**: clickable in the modal (`renderTextWithLinks`); not clickable in the tooltip (acceptable — tooltip is a peek). +- **RTL**: rely on existing logical-property utilities; no hardcoded left/right. + +## Testing + +- **Unit** (`RoomInfoModal.test.tsx`, jsdom env per the app DOM-test convention): + - Renders room name, JID, and full topic text. + - With a short topic, no Show more toggle is present. + - With a long topic (mock `scrollHeight > clientHeight`), the Show more toggle appears and toggling flips to Show less. + - Empty subject renders the placeholder, not a blank block. + - New SDK/app exports used by the modal must be added to the app test mock if applicable (none expected here). +- **Manual (demo mode)**: seed a room with a long multi-line topic (via `DemoClient` / storage), open the room, confirm the header tooltip peek and that clicking the title opens the modal showing the full topic with working collapse and clickable links. + +## Files touched + +- `apps/fluux/src/components/RoomInfoModal.tsx` — new. +- `apps/fluux/src/components/RoomInfoModal.test.tsx` — new. +- `apps/fluux/src/components/RoomHeader.tsx` — modal state, clickable title, tooltip, conditional render. +- i18n: only if a no-topic placeholder key is missing (surgical edit across all locales). + +## Rollout + +Single PR against a feature branch off `main`, squash-merged. No migration, no config, no SDK rebuild required. From 8041f2c72ce01b5e8f066d2c55ce370b38a69f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 10 Jul 2026 13:47:39 +0200 Subject: [PATCH 2/7] docs(plan): room info modal implementation plan (#922) --- .../plans/2026-07-10-room-full-topic-view.md | 348 ++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-room-full-topic-view.md diff --git a/docs/superpowers/plans/2026-07-10-room-full-topic-view.md b/docs/superpowers/plans/2026-07-10-room-full-topic-view.md new file mode 100644 index 00000000..8d872b15 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-room-full-topic-view.md @@ -0,0 +1,348 @@ +# Room Info Modal — View Full Room Topic Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let any room member read a MUC room's full topic/description via a Room Info modal opened from the header, with a hover tooltip for a quick peek (#922). + +**Architecture:** A new self-contained `RoomInfoModal` (built on the existing `ModalShell` primitive) renders the room avatar, JID, and the full subject with a local Show more/less collapse for long text. `RoomHeader`'s name+subject block becomes a keyboard-accessible button that opens the modal and carries a tooltip of the full subject. No SDK, store, or i18n changes. + +**Tech Stack:** React + TypeScript, Tailwind, `react-i18next`, Vitest + Testing Library (jsdom). + +## Global Constraints + +- No SDK changes — the topic value is `room.subject`, already on the `Room` object. +- No new i18n keys — reuse `rooms.topic`, `chat.showMore`, `chat.showLess`; the modal title is the room **name** (not a translated string). +- App unit tests run per-workspace: `cd apps/fluux && npx vitest run `. +- DOM tests pin jsdom via a `@vitest-environment jsdom` docblock at the top of the test file. +- No em-dash connectors in any user-facing copy (none added here, but keep it true). +- Follow existing modal patterns: `ModalShell` for chrome, conditional render alongside the other header modals. + +--- + +### Task 1: `RoomInfoModal` component + +**Files:** +- Create: `apps/fluux/src/components/RoomInfoModal.tsx` +- Test: `apps/fluux/src/components/RoomInfoModal.test.tsx` + +**Interfaces:** +- Consumes: `ModalShell` (`{ title, onClose, width?, children }`), `RoomAvatar` (`{ identifier, name?, avatarUrl?, size? }`), `renderTextWithLinks(text: string)`, `Room` type (`@fluux/sdk`). +- Produces: `export function RoomInfoModal(props: RoomInfoModalProps)` where `interface RoomInfoModalProps { room: Room; onClose: () => void }`. + +- [ ] **Step 1: Write the failing test** + +Create `apps/fluux/src/components/RoomInfoModal.test.tsx`: + +```tsx +// @vitest-environment jsdom +import { describe, it, expect, vi, afterEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { RoomInfoModal } from './RoomInfoModal' +import type { Room } from '@fluux/sdk' + +// Surface i18n keys verbatim so assertions target keys, not translated copy. +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +function makeRoom(overrides: Partial = {}): Room { + return { + jid: 'general@conference.example.com', + name: 'General', + subject: 'Welcome to the general room', + ...overrides, + } as Room +} + +// Helper to force the topic element to report overflow in jsdom (which +// otherwise reports scrollHeight === clientHeight === 0). +function forceOverflow(scrollHeight: number, clientHeight: number) { + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, get() { return scrollHeight }, + }) + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, get() { return clientHeight }, + }) +} + +afterEach(() => { + // Restore jsdom defaults so overflow overrides don't leak between tests. + delete (HTMLElement.prototype as unknown as Record).scrollHeight + delete (HTMLElement.prototype as unknown as Record).clientHeight +}) + +describe('RoomInfoModal', () => { + it('renders the room name (title), JID and full topic', () => { + render( {}} />) + expect(screen.getByText('General')).toBeTruthy() + expect(screen.getByText('general@conference.example.com')).toBeTruthy() + expect(screen.getByText('Welcome to the general room')).toBeTruthy() + expect(screen.getByText('rooms.topic')).toBeTruthy() + }) + + it('omits the topic section when the room has no subject', () => { + render( {}} />) + expect(screen.queryByText('rooms.topic')).toBeNull() + // Identity still renders. + expect(screen.getByText('General')).toBeTruthy() + }) + + it('shows no Show more toggle when the topic fits', () => { + forceOverflow(50, 50) + render( {}} />) + expect(screen.queryByText('chat.showMore')).toBeNull() + expect(screen.queryByText('chat.showLess')).toBeNull() + }) + + it('shows a Show more toggle when the topic overflows, and toggles to Show less', () => { + forceOverflow(300, 120) + render( {}} />) + const moreBtn = screen.getByText('chat.showMore') + expect(moreBtn).toBeTruthy() + fireEvent.click(moreBtn) + expect(screen.getByText('chat.showLess')).toBeTruthy() + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd apps/fluux && npx vitest run src/components/RoomInfoModal.test.tsx` +Expected: FAIL — cannot resolve `./RoomInfoModal` (module not found). + +- [ ] **Step 3: Write the component** + +Create `apps/fluux/src/components/RoomInfoModal.tsx`: + +```tsx +import { useState, useRef, useLayoutEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { ChevronDown, ChevronUp } from 'lucide-react' +import type { Room } from '@fluux/sdk' +import { ModalShell } from './ModalShell' +import { RoomAvatar } from './RoomAvatar' +import { renderTextWithLinks } from '@/utils/messageStyles' + +interface RoomInfoModalProps { + room: Room + onClose: () => void +} + +/** + * Read-only room details for any member: avatar, name (modal title), JID, and + * the full topic/description (`room.subject`). Long topics collapse to six lines + * behind a Show more / Show less toggle. Kept independent of the message-list + * collapse machinery (which needs a messageId + width context) so it stays + * self-contained and testable. + */ +export function RoomInfoModal({ room, onClose }: RoomInfoModalProps) { + return ( + +

+ {/* Identity row */} +
+ +

{room.jid}

+
+ + {/* Topic — only when set */} + {room.subject && } +
+ + ) +} + +function RoomTopic({ subject }: { subject: string }) { + const { t } = useTranslation() + const topicRef = useRef(null) + const [expanded, setExpanded] = useState(false) + const [overflowing, setOverflowing] = useState(false) + + // Measure only while collapsed; when expanded the clamp is off so scrollHeight + // would equal clientHeight. Keeping `overflowing` sticky preserves the toggle. + useLayoutEffect(() => { + const el = topicRef.current + if (!el || expanded) return + setOverflowing(el.scrollHeight > el.clientHeight + 1) + }, [subject, expanded]) + + return ( +
+ + {t('rooms.topic')} + +
+ {renderTextWithLinks(subject)} +
+ {overflowing && ( + + )} +
+ ) +} +``` + +Note: `useTranslation` is imported once and used only inside `RoomTopic` (the modal title is `room.name`, not a translated string). That is intentional — do not add a `useTranslation()` call to `RoomInfoModal`. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd apps/fluux && npx vitest run src/components/RoomInfoModal.test.tsx` +Expected: PASS (4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add apps/fluux/src/components/RoomInfoModal.tsx apps/fluux/src/components/RoomInfoModal.test.tsx +git commit -m "feat(rooms): add RoomInfoModal to view full room topic (#922)" +``` + +--- + +### Task 2: Wire the modal into `RoomHeader` + +**Files:** +- Modify: `apps/fluux/src/components/RoomHeader.tsx` + +**Interfaces:** +- Consumes: `RoomInfoModal` from Task 1 (`{ room, onClose }`), existing `Tooltip`, `useState`. +- Produces: no new exports; behavior change only. + +- [ ] **Step 1: Add the import and modal state** + +In `apps/fluux/src/components/RoomHeader.tsx`, add to the modal imports (near line 17-21): + +```tsx +import { RoomInfoModal } from './RoomInfoModal' +``` + +Add state alongside the existing modal flags (near line 68-70): + +```tsx +const [showInfoModal, setShowInfoModal] = useState(false) +``` + +- [ ] **Step 2: Make the name+subject block open the modal, with a tooltip** + +Replace the name+info block (currently lines 124-130): + +```tsx + {/* Name and info */} +
+

{room.name}

+

+ {room.subject ? renderTextWithLinks(room.subject) : room.jid} +

+
+``` + +with: + +```tsx + {/* Name and info — opens Room Info modal; tooltip peeks the full topic */} + + + +``` + +Note: the `t('rooms.showRoomInfo', 'Room info')` call uses an inline i18n default so no locale edit is required; if the project lint forbids inline defaults, replace with `aria-label="Room info"` (English `aria-label` is acceptable for a non-visual label here, but prefer the inline-default form to match `t('rooms.roomActions', 'Room actions')` already used at line 188). + +- [ ] **Step 3: Conditionally render the modal** + +Add near the other conditionally-rendered modals (after the `showHatsModal` block, ~line 274-278): + +```tsx + {showInfoModal && ( + setShowInfoModal(false)} + /> + )} +``` + +- [ ] **Step 4: Typecheck and lint** + +Run: `npm run typecheck` +Expected: no errors. + +Run: `cd apps/fluux && npx eslint src/components/RoomHeader.tsx src/components/RoomInfoModal.tsx` +Expected: no errors. + +- [ ] **Step 5: Run the room-header-adjacent tests** + +Run: `cd apps/fluux && npx vitest run src/components/RoomInfoModal.test.tsx` +Expected: PASS. + +If a `RoomHeader.test.tsx` exists, run it too and update any snapshot/label assertions the new button introduces: +Run: `cd apps/fluux && npx vitest run src/components/RoomHeader.test.tsx` +Expected: PASS (or updated assertions). + +- [ ] **Step 6: Commit** + +```bash +git add apps/fluux/src/components/RoomHeader.tsx +git commit -m "feat(rooms): open Room Info modal from room header title (#922)" +``` + +--- + +### Task 3: Manual verification in demo mode + +**Files:** none (verification only). + +- [ ] **Step 1: Seed a long topic** + +In demo mode, pick a room and give it a long, multi-line subject. Either edit `apps/fluux/src/demo/DemoClient.ts` where room subjects are seeded, or set it at runtime. A quick runtime option (browser devtools console on the demo page) is to use the room config, but the reliable path is to set a long `subject` on a demo room in the seed data, e.g.: + +```ts +subject: 'This is a deliberately very long room topic that wraps across many lines. '.repeat(8), +``` + +- [ ] **Step 2: Run the demo and verify** + +Run: `npm run dev` then open `http://localhost:5173/demo.html?tutorial=false`. + +Verify: +- Hovering the room title in the header shows a tooltip with the full subject. +- Clicking the room title opens the Room Info modal. +- The modal shows the room name (title), JID, and the full topic. +- The topic collapses to six lines with a "Show more" button; clicking expands it and shows "Show less". +- Links inside the topic are clickable in the modal. +- A room with no subject opens the modal showing name + JID and no topic section. + +- [ ] **Step 3: Final full-suite sanity + finish** + +Run: `cd apps/fluux && npx vitest run src/components/RoomInfoModal.test.tsx` +Run: `npm run typecheck` +Expected: PASS / no errors. + +No commit (verification task). Proceed to open a PR against `main`. + +--- + +## Notes / decisions locked from the spec + +- The members panel (`OccupantPanel`) is intentionally **not** touched — the topic there read as out of place. +- `CollapsibleContent` is intentionally **not** reused — it requires a `messageId`, `expandedMessagesStore`, and `messageWidthContext`, none of which exist outside the message list. +- No-subject rooms omit the topic block rather than showing a placeholder, avoiding a new i18n key across 33 locales. From 5bc3b3c7381900cbcd4205d1699d927d23d082f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 10 Jul 2026 13:49:38 +0200 Subject: [PATCH 3/7] feat(rooms): add RoomInfoModal to view full room topic (#922) --- .../src/components/RoomInfoModal.test.tsx | 69 +++++++++++++++++ apps/fluux/src/components/RoomInfoModal.tsx | 77 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 apps/fluux/src/components/RoomInfoModal.test.tsx create mode 100644 apps/fluux/src/components/RoomInfoModal.tsx diff --git a/apps/fluux/src/components/RoomInfoModal.test.tsx b/apps/fluux/src/components/RoomInfoModal.test.tsx new file mode 100644 index 00000000..877e62aa --- /dev/null +++ b/apps/fluux/src/components/RoomInfoModal.test.tsx @@ -0,0 +1,69 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, afterEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { RoomInfoModal } from './RoomInfoModal' +import type { Room } from '@fluux/sdk' + +// Surface i18n keys verbatim so assertions target keys, not translated copy. +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +function makeRoom(overrides: Partial = {}): Room { + return { + jid: 'general@conference.example.com', + name: 'General', + subject: 'Welcome to the general room', + ...overrides, + } as Room +} + +// Helper to force the topic element to report overflow in jsdom (which +// otherwise reports scrollHeight === clientHeight === 0). +function forceOverflow(scrollHeight: number, clientHeight: number) { + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, get() { return scrollHeight }, + }) + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, get() { return clientHeight }, + }) +} + +afterEach(() => { + // Restore jsdom defaults so overflow overrides don't leak between tests. + delete (HTMLElement.prototype as unknown as Record).scrollHeight + delete (HTMLElement.prototype as unknown as Record).clientHeight +}) + +describe('RoomInfoModal', () => { + it('renders the room name (title), JID and full topic', () => { + render( {}} />) + expect(screen.getByText('General')).toBeTruthy() + expect(screen.getByText('general@conference.example.com')).toBeTruthy() + expect(screen.getByText('Welcome to the general room')).toBeTruthy() + expect(screen.getByText('rooms.topic')).toBeTruthy() + }) + + it('omits the topic section when the room has no subject', () => { + render( {}} />) + expect(screen.queryByText('rooms.topic')).toBeNull() + // Identity still renders. + expect(screen.getByText('General')).toBeTruthy() + }) + + it('shows no Show more toggle when the topic fits', () => { + forceOverflow(50, 50) + render( {}} />) + expect(screen.queryByText('chat.showMore')).toBeNull() + expect(screen.queryByText('chat.showLess')).toBeNull() + }) + + it('shows a Show more toggle when the topic overflows, and toggles to Show less', () => { + forceOverflow(300, 120) + render( {}} />) + const moreBtn = screen.getByText('chat.showMore') + expect(moreBtn).toBeTruthy() + fireEvent.click(moreBtn) + expect(screen.getByText('chat.showLess')).toBeTruthy() + }) +}) diff --git a/apps/fluux/src/components/RoomInfoModal.tsx b/apps/fluux/src/components/RoomInfoModal.tsx new file mode 100644 index 00000000..7e5ac551 --- /dev/null +++ b/apps/fluux/src/components/RoomInfoModal.tsx @@ -0,0 +1,77 @@ +import { useState, useRef, useLayoutEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { ChevronDown, ChevronUp } from 'lucide-react' +import type { Room } from '@fluux/sdk' +import { ModalShell } from './ModalShell' +import { RoomAvatar } from './RoomAvatar' +import { renderTextWithLinks } from '@/utils/messageStyles' + +interface RoomInfoModalProps { + room: Room + onClose: () => void +} + +/** + * Read-only room details for any member: avatar, name (modal title), JID, and + * the full topic/description (`room.subject`). Long topics collapse to six lines + * behind a Show more / Show less toggle. Kept independent of the message-list + * collapse machinery (which needs a messageId + width context) so it stays + * self-contained and testable. + */ +export function RoomInfoModal({ room, onClose }: RoomInfoModalProps) { + return ( + +
+ {/* Identity row */} +
+ +

{room.jid}

+
+ + {/* Topic — only when set */} + {room.subject && } +
+
+ ) +} + +function RoomTopic({ subject }: { subject: string }) { + const { t } = useTranslation() + const topicRef = useRef(null) + const [expanded, setExpanded] = useState(false) + const [overflowing, setOverflowing] = useState(false) + + // Measure only while collapsed; when expanded the clamp is off so scrollHeight + // would equal clientHeight. Keeping `overflowing` sticky preserves the toggle. + useLayoutEffect(() => { + const el = topicRef.current + if (!el || expanded) return + setOverflowing(el.scrollHeight > el.clientHeight + 1) + }, [subject, expanded]) + + return ( +
+ + {t('rooms.topic')} + +
+ {renderTextWithLinks(subject)} +
+ {overflowing && ( + + )} +
+ ) +} From 2cc58536acb92d9094f04b727f63d0326168853f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 10 Jul 2026 13:54:52 +0200 Subject: [PATCH 4/7] feat(rooms): open Room Info modal from room header title (#922) --- apps/fluux/src/components/RoomHeader.tsx | 30 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/fluux/src/components/RoomHeader.tsx b/apps/fluux/src/components/RoomHeader.tsx index caa296fd..2ab95fb3 100644 --- a/apps/fluux/src/components/RoomHeader.tsx +++ b/apps/fluux/src/components/RoomHeader.tsx @@ -19,6 +19,7 @@ import { InviteToRoomModal } from './InviteToRoomModal' import { RoomConfigModal } from './RoomConfigModal' import { RoomMembersModal } from './RoomMembersModal' import { RoomHatsModal } from './RoomHatsModal' +import { RoomInfoModal } from './RoomInfoModal' import { HeaderSubmenuButton } from './header/HeaderSubmenuButton' import { HeaderOverflowKebab, type OverflowEntry } from './header/HeaderOverflowKebab' import { buildNotifyGroup, buildManagementGroup, notifyModeOf } from './header/roomHeaderActions' @@ -68,6 +69,7 @@ export function RoomHeader({ const [showAvatarModal, setShowAvatarModal] = useState(false) const [showMembersModal, setShowMembersModal] = useState(false) const [showHatsModal, setShowHatsModal] = useState(false) + const [showInfoModal, setShowInfoModal] = useState(false) const [avatarError, setAvatarError] = useState(null) const { dragRegionProps } = useWindowDrag() const configModalOpen = useRoomUiStore((s) => s.configModalOpen) @@ -121,13 +123,20 @@ export function RoomHeader({ size="header" /> - {/* Name and info */} -
-

{room.name}

-

- {room.subject ? renderTextWithLinks(room.subject) : room.jid} -

-
+ {/* Name and info — opens Room Info modal; tooltip peeks the full topic */} + + + {/* Notification settings — inline copy (wide tier) */}
@@ -277,6 +286,13 @@ export function RoomHeader({ onClose={() => setShowHatsModal(false)} /> )} + + {showInfoModal && ( + setShowInfoModal(false)} + /> + )} ) } From c05036f8626db09d3f8e6f9e05e2ccc85c083e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 10 Jul 2026 14:08:10 +0200 Subject: [PATCH 5/7] fix(rooms): keep room header title flex sizing + truncation with tooltip (#922) --- apps/fluux/src/components/RoomHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/fluux/src/components/RoomHeader.tsx b/apps/fluux/src/components/RoomHeader.tsx index 2ab95fb3..0a7410e2 100644 --- a/apps/fluux/src/components/RoomHeader.tsx +++ b/apps/fluux/src/components/RoomHeader.tsx @@ -124,12 +124,12 @@ export function RoomHeader({ /> {/* Name and info — opens Room Info modal; tooltip peeks the full topic */} - + diff --git a/apps/fluux/src/components/RoomInfoModal.tsx b/apps/fluux/src/components/RoomInfoModal.tsx index 7e5ac551..5a3af4b6 100644 --- a/apps/fluux/src/components/RoomInfoModal.tsx +++ b/apps/fluux/src/components/RoomInfoModal.tsx @@ -20,8 +20,8 @@ interface RoomInfoModalProps { */ export function RoomInfoModal({ room, onClose }: RoomInfoModalProps) { return ( - -
+ +
{/* Identity row */}
@@ -29,7 +29,7 @@ export function RoomInfoModal({ room, onClose }: RoomInfoModalProps) {
{/* Topic — only when set */} - {room.subject && } + {room.subject?.trim() && }
) @@ -62,6 +62,7 @@ function RoomTopic({ subject }: { subject: string }) {
{overflowing && (