Skip to content

Commit 1055f91

Browse files
authored
Merge pull request #2 from dhawal-ss/agent/managed-homeserver-login
Configure the managed Mesh homeserver and merge the hyperminimal redesign
2 parents d888c49 + e72c9f4 commit 1055f91

94 files changed

Lines changed: 3771 additions & 1578 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-beta.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ jobs:
1919
runs-on: ubuntu-latest
2020
timeout-minutes: 45
2121
env:
22-
VITE_MESH_HOMESERVER: ${{ vars.VITE_MESH_HOMESERVER }}
22+
VITE_MESH_HOMESERVER: https://matrix.mesh.dhawal.org
2323
VITE_MESH_SERVICE_NAME: ${{ vars.VITE_MESH_SERVICE_NAME }}
24+
MESH_MANAGED_HOMESERVER: https://matrix.mesh.dhawal.org
25+
MESH_MANAGED_SERVER_NAME: mesh.dhawal.org
2426
steps:
2527
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
2628

@@ -87,8 +89,10 @@ jobs:
8789
timeout-minutes: 90
8890
environment: matrix-beta
8991
env:
90-
VITE_MESH_HOMESERVER: ${{ vars.VITE_MESH_HOMESERVER }}
92+
VITE_MESH_HOMESERVER: https://matrix.mesh.dhawal.org
9193
VITE_MESH_SERVICE_NAME: ${{ vars.VITE_MESH_SERVICE_NAME }}
94+
MESH_MANAGED_HOMESERVER: https://matrix.mesh.dhawal.org
95+
MESH_MANAGED_SERVER_NAME: mesh.dhawal.org
9296
permissions:
9397
contents: write
9498
steps:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ e2e-results/
2929
# Internal documents, prompts, chats, and design artifacts
3030
*.md
3131
*.mdx
32+
!AGENTS.md
33+
!mesh/infra/homeserver/README.md
3234
*.doc
3335
*.docx
3436
*.pages
@@ -78,6 +80,7 @@ claudereview/
7880

7981
# Generated local integration-test state
8082
**/infra/matrix-spike/runtime/
83+
**/infra/homeserver/runtime/
8184

8285
# Local configuration and secrets
8386
.env

AGENTS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Mesh Product North Star
2+
3+
Mesh must feel like a regular consumer communication app, even though it is
4+
built on decentralized infrastructure. A normal user should be able to:
5+
6+
1. install Mesh;
7+
2. open an invitation;
8+
3. create or sign in to an account; and
9+
4. enter the community.
10+
11+
That path must not require the user to understand or configure Matrix,
12+
homeservers, federation, DNS, TLS, ports, relays, TURN, Synapse, or storage
13+
replication.
14+
15+
## Non-negotiable UX rules
16+
17+
- Ship safe, working defaults for the recommended Mesh service in release
18+
builds.
19+
- Invitation links must carry or resolve everything needed to reach the
20+
service and community.
21+
- Use plain product language such as "service", "community", and "voice";
22+
reserve protocol terminology for diagnostics and advanced settings.
23+
- Keep "sign in somewhere else" and custom homeserver support available as an
24+
advanced path so decentralization does not become lock-in.
25+
- Detect service capabilities and network conditions automatically. Do not ask
26+
users to make infrastructure decisions that Mesh can make safely.
27+
- Errors must explain what the user can do next without exposing raw protocol
28+
failures as the primary message.
29+
- Never silently consume a user's disk, bandwidth, or battery for shared
30+
storage. Contribution must be explicit, bounded, encrypted, observable, and
31+
reversible.
32+
- Treat extra setup steps in the default onboarding path as product defects
33+
unless they are required for account security or informed consent.
34+
35+
## Architecture guardrails
36+
37+
- The managed Mesh homeserver is the zero-configuration default, not the only
38+
compatible service.
39+
- Keep identity, membership, permissions, text history, encryption state, and
40+
synchronization on the Matrix-compatible control plane.
41+
- Treat peer-assisted storage as an optional encrypted data plane with durable
42+
anchors, replication targets, integrity verification, repair, quotas, and
43+
garbage collection.
44+
- Preserve standard Matrix interoperability wherever possible so users and
45+
communities can bring another compatible homeserver.
46+
- Infrastructure choices must be migration-ready: stable identity domains,
47+
backed-up signing keys, portable databases/media, and configuration separate
48+
from secrets.
49+
50+
## Product references
51+
52+
Cinny is a useful reference for its calm interface, familiar
53+
community/channel organization, and configuration-driven homeserver defaults.
54+
Mesh should learn from that simplicity while hiding more infrastructure from
55+
the recommended user path. Do not copy Cinny branding or code without an
56+
explicit compatibility and licensing review.

mesh/e2e/authenticated-shell.spec.ts

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,14 @@ async function installAuthenticatedMatrixMock(page: Page): Promise<void> {
273273
}
274274
case 'matrix_typing_users':
275275
return []
276+
case 'matrix_room_pins':
277+
return {
278+
roomId: String(args.roomId),
279+
eventIds: [],
280+
messages: [],
281+
unavailableEventIds: [],
282+
canManage: true,
283+
}
276284
case 'matrix_mark_read':
277285
case 'matrix_set_typing':
278286
case 'matrix_save_composer_draft':
@@ -423,14 +431,38 @@ test.describe('authenticated desktop shell', () => {
423431

424432
const context = page.getByRole('complementary', { name: 'Room context for general' })
425433
await expect(context).toBeVisible()
426-
await expect(context.getByRole('tab', { name: 'Ledger' })).toHaveAttribute('aria-selected', 'true')
434+
const peopleTab = context.getByRole('tab', { name: 'People' })
435+
const ledgerTab = context.getByRole('tab', { name: 'Ledger' })
436+
const filesTab = context.getByRole('tab', { name: 'Files' })
437+
const pinsTab = context.getByRole('tab', { name: 'Pins' })
438+
await expect(ledgerTab).toHaveAttribute('aria-selected', 'true')
439+
await expect(ledgerTab).toHaveAttribute('tabindex', '0')
440+
await expect(peopleTab).toHaveAttribute('tabindex', '-1')
441+
await expect(filesTab).toHaveAttribute('tabindex', '-1')
442+
await expect(pinsTab).toHaveAttribute('tabindex', '-1')
427443
await expect(context.getByText('Protected end to end')).toBeVisible()
428444
await expect(context.getByText('1 need review')).toBeVisible()
429445
await expect(context.getByText('Ready', { exact: true })).toBeVisible()
430446

431-
await context.getByRole('tab', { name: 'Pins' }).click()
447+
await ledgerTab.focus()
448+
await page.keyboard.press('ArrowRight')
449+
await expect(filesTab).toBeFocused()
450+
await expect(filesTab).toHaveAttribute('aria-selected', 'true')
451+
await expect(context.getByRole('tabpanel')).toHaveAttribute(
452+
'aria-labelledby',
453+
await filesTab.getAttribute('id') ?? '',
454+
)
455+
await page.keyboard.press('End')
456+
await expect(pinsTab).toBeFocused()
457+
await expect(pinsTab).toHaveAttribute('aria-selected', 'true')
432458
await expect(context.getByText('Nothing pinned yet')).toBeVisible()
433-
await context.getByRole('tab', { name: 'Files' }).click()
459+
await page.keyboard.press('Home')
460+
await expect(peopleTab).toBeFocused()
461+
await expect(peopleTab).toHaveAttribute('aria-selected', 'true')
462+
await page.keyboard.press('ArrowLeft')
463+
await expect(pinsTab).toBeFocused()
464+
await page.keyboard.press('ArrowLeft')
465+
await expect(filesTab).toBeFocused()
434466
await expect(context.getByText('No files shared yet')).toBeVisible()
435467

436468
await context.getByRole('button', { name: 'Close room context' }).click()
@@ -473,6 +505,27 @@ test.describe('authenticated desktop shell', () => {
473505
})
474506
})
475507

508+
test('hands Security focus back to the persistent User Settings trigger', async ({ page }) => {
509+
await openAuthenticatedShell(page)
510+
511+
const settingsButton = page.getByRole('button', { name: 'User settings' })
512+
await settingsButton.click()
513+
const settingsDialog = page.getByRole('dialog', { name: 'User Settings' })
514+
await expect(settingsDialog).toBeVisible()
515+
516+
await settingsDialog.getByRole('button', { name: 'Open your devices' }).click()
517+
const securityDialog = page.getByRole('dialog', { name: 'Your devices' })
518+
await expect(settingsDialog).toHaveCount(0)
519+
await expect(securityDialog).toBeVisible()
520+
await expect(page.getByRole('dialog')).toHaveCount(1)
521+
522+
await page.keyboard.press('Escape')
523+
524+
await expect(securityDialog).toHaveCount(0)
525+
await expect(settingsButton).toBeFocused()
526+
expect(await page.evaluate(() => document.activeElement === document.body)).toBe(false)
527+
})
528+
476529
test('opens community management as an accessible sheet and restores focus', async ({ page }) => {
477530
await openAuthenticatedShell(page)
478531

@@ -567,16 +620,42 @@ test.describe('authenticated desktop shell', () => {
567620
})
568621

569622
test.describe('authenticated narrow shell', () => {
570-
test.use({ viewport: { width: 390, height: 844 } })
623+
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true })
571624

572625
test('@a11y has no automated WCAG A/AA violations with navigation open', async ({ page }) => {
573626
await openAuthenticatedShell(page)
574627
await page.getByRole('button', { name: 'Open room navigation' }).click()
575628
await expect(page.getByRole('button', { name: 'Close room navigation' })).toBeVisible()
629+
const navigationDrawer = page.locator('#mesh-context-sidebar')
630+
await expect(navigationDrawer).toHaveAttribute('aria-modal', 'true')
631+
expect(await navigationDrawer.evaluate((drawer) => drawer.contains(document.activeElement))).toBe(true)
632+
for (let index = 0; index < 10; index += 1) await page.keyboard.press('Tab')
633+
expect(await navigationDrawer.evaluate((drawer) => drawer.contains(document.activeElement))).toBe(true)
576634

577635
await expectNoWcagViolations(page, 'Authenticated narrow shell with room navigation')
578636
})
579637

638+
test('closes room navigation with Escape and restores its trigger', async ({ page }) => {
639+
await openAuthenticatedShell(page)
640+
const drawerButton = page.getByRole('button', { name: 'Open room navigation' })
641+
await drawerButton.click()
642+
await expect(page.locator('#mesh-context-sidebar')).toHaveAttribute('data-open', 'true')
643+
644+
await page.keyboard.press('Escape')
645+
646+
await expect(page.locator('#mesh-context-sidebar')).toHaveAttribute('data-open', 'false')
647+
await expect(drawerButton).toBeFocused()
648+
})
649+
650+
test('keeps message actions reachable on touch-only layouts', async ({ page }) => {
651+
await openAuthenticatedShell(page)
652+
const actions = page.locator('.mesh-message-actions').first()
653+
654+
await expect(actions).toHaveCSS('opacity', '1')
655+
await expect(actions).toHaveCSS('pointer-events', 'auto')
656+
await expect(actions.getByRole('button', { name: /React to message/ })).toBeVisible()
657+
})
658+
580659
test('opens the room drawer, changes rooms, and sends a message without overflow', async ({ page }) => {
581660
await openAuthenticatedShell(page)
582661

@@ -617,7 +696,20 @@ test.describe('authenticated narrow shell', () => {
617696
const context = page.getByRole('complementary', { name: 'Room context for general' })
618697
await expect(context).toBeVisible()
619698
await expect(page.locator('.mesh-room-context-backdrop')).toBeVisible()
620-
await context.getByRole('tab', { name: 'Ledger' }).click()
699+
expect(await context.evaluate((panel) => panel.contains(document.activeElement))).toBe(true)
700+
for (let index = 0; index < 8; index += 1) await page.keyboard.press('Tab')
701+
expect(await context.evaluate((panel) => panel.contains(document.activeElement))).toBe(true)
702+
const ledgerTab = context.getByRole('tab', { name: 'Ledger' })
703+
const peopleTab = context.getByRole('tab', { name: 'People' })
704+
const pinsTab = context.getByRole('tab', { name: 'Pins' })
705+
await ledgerTab.focus()
706+
await page.keyboard.press('ArrowLeft')
707+
await expect(peopleTab).toBeFocused()
708+
await page.keyboard.press('ArrowLeft')
709+
await expect(pinsTab).toBeFocused()
710+
await page.keyboard.press('Home')
711+
await expect(peopleTab).toBeFocused()
712+
await ledgerTab.click()
621713
await expect(context.getByText('Protected end to end')).toBeVisible()
622714
await expectNoWcagViolations(page, 'Authenticated narrow room context drawer')
623715

@@ -643,6 +735,28 @@ test.describe('authenticated narrow shell', () => {
643735
await expect(dialog).toHaveCount(0)
644736
})
645737

738+
test('hands narrow Security focus back to the persistent User Settings trigger', async ({ page }) => {
739+
await openAuthenticatedShell(page)
740+
741+
await page.getByRole('button', { name: 'Open room navigation' }).click()
742+
const settingsButton = page.getByRole('button', { name: 'User settings' })
743+
await settingsButton.click()
744+
const settingsDialog = page.getByRole('dialog', { name: 'User Settings' })
745+
await expect(settingsDialog).toBeVisible()
746+
747+
await settingsDialog.getByRole('button', { name: 'Open your devices' }).click()
748+
const securityDialog = page.getByRole('dialog', { name: 'Your devices' })
749+
await expect(settingsDialog).toHaveCount(0)
750+
await expect(securityDialog).toBeVisible()
751+
await expect(page.getByRole('dialog')).toHaveCount(1)
752+
753+
await page.keyboard.press('Escape')
754+
755+
await expect(securityDialog).toHaveCount(0)
756+
await expect(settingsButton).toBeFocused()
757+
expect(await page.evaluate(() => document.activeElement === document.body)).toBe(false)
758+
})
759+
646760
test('fits community management to the viewport and restores its drawer trigger', async ({ page }) => {
647761
await openAuthenticatedShell(page)
648762

mesh/e2e/design-system.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import { expectNoWcagViolations } from './helpers/accessibility'
33

44
const runtimeErrors = new WeakMap<Page, string[]>()
55

6+
function relativeLuminance(hex: string): number {
7+
const channels = [1, 3, 5].map((index) => Number.parseInt(hex.slice(index, index + 2), 16) / 255)
8+
const [red, green, blue] = channels.map((channel) =>
9+
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,
10+
)
11+
return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue)
12+
}
13+
14+
function contrastRatio(foreground: string, background: string): number {
15+
const foregroundLuminance = relativeLuminance(foreground)
16+
const backgroundLuminance = relativeLuminance(background)
17+
return (
18+
(Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
19+
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05)
20+
)
21+
}
22+
623
async function sampleAmbientMotion(page: Page): Promise<string[]> {
724
const probe = page.locator('[data-ambient-motion-probe]')
825
await expect(probe).toBeAttached()
@@ -56,6 +73,46 @@ test('renders every supported theme and exposes keyboard-operable primitives', a
5673
await expect(page.getByRole('tabpanel').getByText('Detail content')).toBeVisible()
5774
})
5875

76+
test('keeps semantic foregrounds and focus indicators contrast-safe in light theme', async ({ page }) => {
77+
const lightTheme = page.locator('section[data-theme="light"]')
78+
const tokens = await lightTheme.evaluate((element) => {
79+
const styles = getComputedStyle(element)
80+
const value = (name: string) => styles.getPropertyValue(name).trim()
81+
return {
82+
canvas: value('--surface-canvas'),
83+
accent: value('--content-accent'),
84+
link: value('--content-link'),
85+
focus: value('--border-focus'),
86+
success: value('--status-success'),
87+
warning: value('--status-warning'),
88+
avatar: value('--avatar-sand'),
89+
onAvatar: value('--content-on-avatar'),
90+
}
91+
})
92+
93+
expect(contrastRatio(tokens.accent, tokens.canvas)).toBeGreaterThanOrEqual(4.5)
94+
expect(contrastRatio(tokens.link, tokens.canvas)).toBeGreaterThanOrEqual(4.5)
95+
expect(contrastRatio(tokens.success, tokens.canvas)).toBeGreaterThanOrEqual(4.5)
96+
expect(contrastRatio(tokens.warning, tokens.canvas)).toBeGreaterThanOrEqual(4.5)
97+
expect(contrastRatio(tokens.focus, tokens.canvas)).toBeGreaterThanOrEqual(3)
98+
expect(contrastRatio(tokens.onAvatar, tokens.avatar)).toBeGreaterThanOrEqual(4.5)
99+
})
100+
101+
test('renders the command palette as one compact surface', async ({ page }) => {
102+
await page.getByRole('button', { name: /Commands/ }).click()
103+
104+
const dialog = page.getByRole('dialog', { name: 'Command palette' })
105+
const input = dialog.getByRole('combobox', { name: 'Command palette' })
106+
const listbox = dialog.getByRole('listbox')
107+
await expect(dialog).toBeVisible()
108+
await expect(input).toBeFocused()
109+
await expect(listbox).toBeVisible()
110+
await expect(dialog).toHaveCSS('padding', '0px')
111+
await expect(input).toHaveCSS('border-top-width', '0px')
112+
await expect(listbox).toHaveCSS('position', 'static')
113+
await expect(listbox).toHaveCSS('box-shadow', 'none')
114+
})
115+
59116
test('runs ambient motion when the operating system allows it', async ({ page }) => {
60117
await page.emulateMedia({ reducedMotion: 'no-preference' })
61118
await page.reload()

mesh/e2e/matrix-messaging.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,15 @@ test.describe('Matrix direct messaging and encrypted attachments', () => {
456456
await expect(page.getByText('Message backup')).toBeVisible()
457457

458458
await page.getByRole('button', { name: 'Review devices and backup' }).click()
459-
await expect(page.getByRole('dialog', { name: 'Your devices' })).toBeVisible()
459+
const securityDialog = page.getByRole('dialog', { name: 'Your devices' })
460+
await expect(securityDialog).toBeVisible()
461+
await expect(page.getByRole('dialog')).toHaveCount(1)
460462
await expect(page.getByText('Message backup is ready')).toBeVisible()
463+
464+
await page.keyboard.press('Escape')
465+
await expect(securityDialog).toHaveCount(0)
466+
await expect(trustSummary).toBeFocused()
467+
expect(await page.evaluate(() => document.activeElement === document.body)).toBe(false)
461468
})
462469

463470
test('sends DM text through the dedicated Matrix direct-message command', async ({ page }) => {

mesh/e2e/message-keyboard-actions.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ async function installKeyboardActionsMock(page: Page): Promise<void> {
186186
}
187187
case 'matrix_typing_users':
188188
return []
189+
case 'matrix_room_pins':
190+
return {
191+
roomId: String(args.roomId),
192+
eventIds: [],
193+
messages: [],
194+
unavailableEventIds: [],
195+
canManage: true,
196+
}
189197
case 'matrix_mark_read':
190198
case 'matrix_set_typing':
191199
case 'matrix_save_composer_draft':

0 commit comments

Comments
 (0)