diff --git a/apps/fluux/src/components/RoomHeader.tsx b/apps/fluux/src/components/RoomHeader.tsx index caa296fd..31cf75e7 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)} + /> + )} ) } 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..5a3af4b6 --- /dev/null +++ b/apps/fluux/src/components/RoomInfoModal.tsx @@ -0,0 +1,78 @@ +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?.trim() && } +
+
+ ) +} + +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 && ( + + )} +
+ ) +} diff --git a/apps/fluux/src/components/__snapshots__/RoomView.test.tsx.snap b/apps/fluux/src/components/__snapshots__/RoomView.test.tsx.snap index f9c54890..00e123e0 100644 --- a/apps/fluux/src/components/__snapshots__/RoomView.test.tsx.snap +++ b/apps/fluux/src/components/__snapshots__/RoomView.test.tsx.snap @@ -20,16 +20,22 @@ exports[`RoomView > Snapshots > should match snapshot for non-joined room 1`] =
-

- Test Room -

-

- room@conference.example.com -

+ + Test Room + +

+ room@conference.example.com +

+