diff --git a/CHANGELOG.md b/CHANGELOG.md
index f762ef469a..e7e2cac202 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@
### Fixes
* Fix: leaving a community now purges uploaded and downloaded files; if the leave is interrupted (process killed, OS-terminated, power loss), the purge is finished on the next app launch [#3225](https://github.com/TryQuiet/quiet/issues/3225)
+* Fixed settings section titles rendering inside the scrollable content; moved section titles to the settings header and added a regression test to prevent future regressions [#2568](https://github.com/TryQuiet/quiet/issues/2568)
* Fixed android not requesting permission for foreground push notifications [#3254](https://github.com/TryQuiet/quiet/issues/3254)
* Fixes race condition with android push notifications [#3255](https://github.com/TryQuiet/quiet/issues/3255)
* Mark IOS UI as needing compatibility updates to fix contrast problems on IOS 26 [#3266](https://github.com/TryQuiet/quiet/issues/3266)
diff --git a/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx b/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx
new file mode 100644
index 0000000000..0f30da8042
--- /dev/null
+++ b/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx
@@ -0,0 +1,245 @@
+import React from 'react'
+import { render, screen, fireEvent, within, waitFor } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import { SettingsComponent } from './SettingsComponent'
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+const noOp = jest.fn()
+
+const mockLeaveCommunityModal = {
+ open: false,
+ handleOpen: jest.fn(),
+ handleClose: jest.fn(),
+}
+
+const MockTab: React.FC<{ handleClose: () => void }> = ({ handleClose }) => (
+
+
Tab content
+
+
+)
+
+const defaultTabs: Record void }>> = {
+ about: MockTab,
+ notifications: MockTab,
+ attachments: MockTab,
+ invite: MockTab,
+ qrcode: MockTab,
+ leaveCommunity: MockTab,
+ debug: MockTab,
+}
+
+function renderSettings(overrides: Partial> = {}) {
+ return render(
+
+ )
+}
+
+// Click a menu item and wait for the tab drawer to fully mount.
+// MUI Drawer only inserts children into the DOM when open=true, so we must
+// wait after the state-updating click before querying the second drawer.
+async function openTab(testId: string) {
+ fireEvent.click(screen.getByTestId(testId))
+ return screen.findByTestId('ArrowBackIcon') // resolves once drawer is mounted
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe('SettingsComponent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ // ── Rendering ─────────────────────────────────────────────────────────────
+
+ describe('main drawer', () => {
+ it('renders the "Community Settings" heading when open', () => {
+ renderSettings()
+ expect(screen.getByText('Community Settings')).toBeInTheDocument()
+ })
+
+ it('renders all expected menu items', () => {
+ renderSettings()
+ expect(screen.getByTestId('about-settings-tab')).toBeInTheDocument()
+ expect(screen.getByTestId('notifications-settings-tab')).toBeInTheDocument()
+ expect(screen.getByTestId('attachments-settings-tab')).toBeInTheDocument()
+ expect(screen.getByTestId('invite-settings-tab')).toBeInTheDocument()
+ expect(screen.getByTestId('qr-code-settings-tab')).toBeInTheDocument()
+ })
+
+ it('calls handleClose when the close button is clicked', () => {
+ const handleClose = jest.fn()
+ renderSettings({ handleClose })
+ fireEvent.click(screen.getByTestId('CloseIcon'))
+ expect(handleClose).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ // ── Tab drawer – issue #2568 regression ───────────────────────────────────
+
+ describe('issue #2568 – tab title in top bar, not in content area', () => {
+ /**
+ * When a tab is opened the title must appear inside the header ListItem
+ * (the element that also contains the back-arrow button), NOT inside the
+ * scrollable content area below the divider.
+ *
+ * Both drawers stay in the DOM simultaneously once the tab is open, so
+ * some title strings (e.g. "Notifications") appear twice – once as a menu
+ * item in drawer 1 and once as the header title in drawer 2. We therefore
+ * scope every assertion to the specific container.
+ */
+ it.each([
+ ['about', 'About Quiet'],
+ ['notifications', 'Notifications'],
+ ['attachments', 'Files and Images'],
+ ['invite', 'Add Members'],
+ ['qr-code', 'QR Code'],
+ ])('"%s" tab shows "%s" in the header bar', async (testId, expectedTitle) => {
+ renderSettings()
+
+ // openTab uses fireEvent + findByTestId (async) to wait for the second
+ // drawer to mount – fireEvent reliably triggers MUI's onClick handlers.
+ const backButton = await openTab(`${testId}-settings-tab`)
+
+ // The title must be inside the same ListItem as the back button (= the header bar)
+ const headerListItem = backButton.closest('li') as HTMLElement
+ expect(headerListItem).not.toBeNull()
+ expect(within(headerListItem).getByText(expectedTitle)).toBeInTheDocument()
+
+ // The scrollable content area must NOT contain the title
+ const tabContent = screen.getByTestId('mock-tab-content')
+ expect(within(tabContent).queryByText(expectedTitle)).not.toBeInTheDocument()
+ })
+ })
+
+ // ── Tab navigation ────────────────────────────────────────────────────────
+
+ describe('tab navigation', () => {
+ it('opens the tab drawer when a menu item is clicked', async () => {
+ renderSettings()
+ expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument()
+
+ await openTab('notifications-settings-tab')
+
+ expect(screen.getByTestId('ArrowBackIcon')).toBeInTheDocument()
+ })
+
+ it('renders the tab component content after opening a tab', async () => {
+ renderSettings()
+ await openTab('about-settings-tab')
+
+ expect(screen.getByTestId('mock-tab-content')).toBeInTheDocument()
+ })
+
+ it('closes the tab drawer when the back button is clicked', async () => {
+ renderSettings()
+ const backButton = await openTab('notifications-settings-tab')
+
+ fireEvent.click(backButton)
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument()
+ })
+ })
+
+ it('closes the tab drawer when the tab itself calls handleClose', async () => {
+ renderSettings()
+ await openTab('notifications-settings-tab')
+
+ fireEvent.click(screen.getByText('Close from tab'))
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('mock-tab-content')).not.toBeInTheDocument()
+ })
+ })
+
+ it('shows the correct title when switching between tabs', async () => {
+ renderSettings()
+
+ // Open notifications and verify its title is in the header
+ const backButton1 = await openTab('notifications-settings-tab')
+ const header1 = backButton1.closest('li') as HTMLElement
+ expect(within(header1).getByText('Notifications')).toBeInTheDocument()
+
+ // Go back to the main menu
+ fireEvent.click(backButton1)
+ await waitFor(() => {
+ expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument()
+ })
+
+ // Open attachments and verify its title
+ const backButton2 = await openTab('attachments-settings-tab')
+ const header2 = backButton2.closest('li') as HTMLElement
+ expect(within(header2).getByText('Files and Images')).toBeInTheDocument()
+ })
+ })
+
+ // ── isWindows prop ────────────────────────────────────────────────────────
+
+ describe('isWindows prop', () => {
+ it('hides the "Leave community" item on Windows', () => {
+ renderSettings({ isWindows: true })
+ expect(screen.queryByTestId('leave-community-settings-tab')).not.toBeInTheDocument()
+ })
+
+ it('shows the "Leave community" item on non-Windows platforms', () => {
+ renderSettings({ isWindows: false })
+ expect(screen.getByTestId('leave-community-settings-tab')).toBeInTheDocument()
+ })
+
+ it('shows the "Leave community" item when isWindows is undefined', () => {
+ renderSettings({ isWindows: undefined })
+ expect(screen.getByTestId('leave-community-settings-tab')).toBeInTheDocument()
+ })
+ })
+
+ // ── Debug tab (environment-gated) ─────────────────────────────────────────
+
+ describe('debug tab', () => {
+ const originalEnv = process.env
+
+ afterEach(() => {
+ process.env = { ...originalEnv }
+ })
+
+ it('shows the debug tab in development mode', () => {
+ process.env = { ...originalEnv, NODE_ENV: 'development' }
+ renderSettings()
+ expect(screen.getByTestId('debug-settings-tab')).toBeInTheDocument()
+ })
+
+ it('shows the debug tab when IS_E2E is "true"', () => {
+ process.env = { ...originalEnv, NODE_ENV: 'production', IS_E2E: 'true' }
+ renderSettings()
+ expect(screen.getByTestId('debug-settings-tab')).toBeInTheDocument()
+ })
+
+ it('hides the debug tab in production without IS_E2E', () => {
+ process.env = { ...originalEnv, NODE_ENV: 'production', IS_E2E: undefined }
+ renderSettings()
+ expect(screen.queryByTestId('debug-settings-tab')).not.toBeInTheDocument()
+ })
+ })
+
+ // ── Closed state ──────────────────────────────────────────────────────────
+
+ describe('closed state', () => {
+ it('does not mount main drawer content when open is false', () => {
+ // MUI Drawer unmounts its children when open=false (no keepMounted),
+ // so queryByText returns null rather than a hidden element.
+ renderSettings({ open: false })
+ expect(screen.getByText('Community Settings')).not.toBeVisible()
+ })
+ })
+})
diff --git a/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx b/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx
index b23486e960..6483964203 100644
--- a/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx
+++ b/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx
@@ -14,6 +14,16 @@ const classes = {
leaveComunity: `${PREFIX}leaveCommunity`,
}
+const TAB_TITLES: Record = {
+ about: 'About Quiet',
+ notifications: 'Notifications',
+ attachments: 'Files and Images',
+ invite: 'Add Members',
+ qrcode: 'QR Code',
+ leaveCommunity: 'Leave community',
+ debug: 'Debug Information',
+}
+
export interface SettingsComponentProps {
open: boolean
handleClose: () => void
@@ -47,16 +57,12 @@ export const SettingsComponent: React.FC = ({
return (
<>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
Community Settings
@@ -102,7 +108,7 @@ export const SettingsComponent: React.FC = ({
className={classes.leaveComunity}
onClick={() => handleChange('leaveCommunity')}
>
- Leave community
+ Leave community
@@ -118,17 +124,29 @@ export const SettingsComponent: React.FC = ({
)}
-
-
- {currentTab !== '' ? : }
-
-
-
- {TabComponent && }
+ theme.zIndex.modal + 1 }}
+ >
+
+
+
+
+
+
+
+ {TAB_TITLES[currentTab]}
+
+
+
+
+
+ {TabComponent && }
+
>
diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx
index 15855d883a..f693acad5a 100644
--- a/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx
+++ b/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx
@@ -154,11 +154,6 @@ export const AttachmentsComponent: React.FC = ({ maxAutodownlo
return (
-
-
- Files and Images
-
-
Auto-download...
diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx
index 0b654a46e5..6b09aa2a24 100644
--- a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx
+++ b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx
@@ -99,13 +99,6 @@ export const InviteComponent: FC = ({
}
return (
-
-
-
- Add Members
-
-
-
Your community link
diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx
index ef8130bfa8..85c9182798 100644
--- a/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx
+++ b/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx
@@ -121,11 +121,6 @@ export const NotificationsComponent: React.FC = ({
}) => {
return (
-
-
- Notifications
-
-
Notify me about...
diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx
index ba6c1a22df..f08b990324 100644
--- a/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx
+++ b/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx
@@ -34,10 +34,7 @@ export const QRCodeComponent: FC = ({ value }) => {
return (
- Only admins can invite new members
-
-
-
+
Only admins can invite new members to this community. Ask the community creator for a QR code to share.
@@ -45,22 +42,19 @@ export const QRCodeComponent: FC = ({ value }) => {
)
}
return (
-
-
-
-
-
-
-
- Invitation QR code
-
-
-
- This community QR code is private. If it is shared with someone, they can scan it with their camera to
- join this community.
-
-
-
+
+
+
+
+
+
+ This community QR code is private. If it is shared with someone, they can scan it with their camera to join
+ this community.
+
)
diff --git a/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx b/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx
index cf52edc997..be0262fc88 100644
--- a/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx
+++ b/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx
@@ -204,12 +204,6 @@ export const DebugInfoComponent: React.FC = () => {
return (
-
-
- Debug Information
-
-
-
Summary
diff --git a/packages/desktop/src/renderer/components/widgets/Settings/About.tsx b/packages/desktop/src/renderer/components/widgets/Settings/About.tsx
index 6be554d6f3..b5d3d45e8e 100644
--- a/packages/desktop/src/renderer/components/widgets/Settings/About.tsx
+++ b/packages/desktop/src/renderer/components/widgets/Settings/About.tsx
@@ -24,19 +24,12 @@ export const About: FC = () => {
const version = app.getVersion()
return (
-
-
- About Quiet
-
-
-
-
- Version: {version}
-
- Copyright © {new Date().getFullYear()} Quiet LLC and other contributors
-
-
+
+ Version: {version}
+
+ Copyright © {new Date().getFullYear()} Quiet LLC and other contributors
+
)