From 7f5aee78e9c7b6bbf9395cbfda16537d3aaa4b6c Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Mon, 20 Jul 2026 23:27:15 +0200 Subject: [PATCH 1/2] feat(mobile): render global modals as full-screen pages Search, create room, create space, and bug report open as full-screen pages on mobile via useOpen*Modal hooks checking useScreenSizeContext. Desktop modal flow unchanged. Adds full-screen room/space settings with shared section layout and swipe-back navigation. Adds tauri-plugin-sharekit (v0.4.0-rc.5) for iOS/Android system share sheet, with a shareText() util that falls back to Web Share API then clipboard. Wired into ShareChip, Space menu, SpaceTabs menu. Syncs Cargo.lock with the new sharekit dependency and relaxes the edge-to-edge inset contract test to match the wired plugin via .plugin(tauri_plugin_edge_to_edge::init()) so chaining sharekit onto the same mobile builder no longer breaks the test. --- .changeset/mobile-full-screen-pages.md | 5 + package.json | 1 + pnpm-lock.yaml | 10 + src-tauri/Cargo.lock | 32 + src-tauri/Cargo.toml | 1 + src-tauri/capabilities/mobile.json | 3 +- src-tauri/src/lib.rs | 4 +- src/app/components/Modal500.test.tsx | 9 +- src/app/components/Modal500.tsx | 12 + src/app/components/page/Page.tsx | 5 +- .../components/page/SettingsSectionPage.tsx | 57 ++ src/app/components/page/index.tsx | 1 + src/app/components/page/style.css.ts | 20 +- src/app/components/user-profile/UserChips.tsx | 21 + .../features/bug-report/BugReportModal.tsx | 558 +++++++++--------- src/app/features/bug-report/index.ts | 2 +- .../common-settings/appearance/Appearance.tsx | 38 +- .../common-settings/cosmetics/Cosmetics.tsx | 23 +- .../developer-tools/DevelopTools.tsx | 31 +- .../emojis-stickers/EmojisStickers.tsx | 30 +- .../common-settings/members/Members.tsx | 27 +- src/app/features/navigate/NavigateModal.tsx | 12 +- .../features/room-settings/RoomSettings.tsx | 25 +- .../abbreviations/RoomAbbreviations.tsx | 29 +- .../room-settings/general/General.tsx | 26 +- .../room-settings/permissions/Permissions.tsx | 26 +- src/app/features/settings/Settings.tsx | 4 +- .../features/settings/SettingsRoute.test.tsx | 5 +- .../features/settings/SettingsSectionPage.tsx | 58 +- src/app/features/settings/styles.css.ts | 22 +- .../features/space-settings/SpaceSettings.tsx | 200 ++++--- .../space-settings/general/General.tsx | 26 +- .../permissions/Permissions.tsx | 26 +- src/app/generated/tauri/types.ts | 1 - src/app/pages/Router.tsx | 26 + .../pages/client/bug-report/BugReportPage.tsx | 102 ++++ src/app/pages/client/bug-report/index.ts | 1 + .../client/create-room/CreateRoomPage.tsx | 144 +++++ src/app/pages/client/create-room/index.ts | 1 + src/app/pages/client/create/Create.tsx | 42 +- src/app/pages/client/sidebar/SpaceTabs.tsx | 14 + src/app/pages/client/space/Space.tsx | 14 + src/app/pages/pathUtils.ts | 12 + src/app/pages/paths.ts | 2 + src/app/state/hooks/bugReportModal.ts | 13 +- src/app/state/hooks/createRoomModal.ts | 11 +- src/app/state/hooks/createSpaceModal.ts | 13 +- src/app/styles/edgeToEdgeInsets.test.ts | 2 +- src/app/utils/share.ts | 32 + 49 files changed, 1091 insertions(+), 688 deletions(-) create mode 100644 .changeset/mobile-full-screen-pages.md create mode 100644 src/app/components/page/SettingsSectionPage.tsx create mode 100644 src/app/pages/client/bug-report/BugReportPage.tsx create mode 100644 src/app/pages/client/bug-report/index.ts create mode 100644 src/app/pages/client/create-room/CreateRoomPage.tsx create mode 100644 src/app/pages/client/create-room/index.ts create mode 100644 src/app/utils/share.ts diff --git a/.changeset/mobile-full-screen-pages.md b/.changeset/mobile-full-screen-pages.md new file mode 100644 index 0000000000..27fcd8a7ab --- /dev/null +++ b/.changeset/mobile-full-screen-pages.md @@ -0,0 +1,5 @@ +--- +sable: patch +--- + +Render the global modals (search, create room, create space, bug report) as full-screen pages on mobile instead of centered modals. diff --git a/package.json b/package.json index deaa0cc1d6..df86181fed 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^3.0.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0", "@choochmeque/tauri-plugin-notifications-api": "0.5.0-rc.11", + "@choochmeque/tauri-plugin-sharekit-api": "0.4.0-rc.5", "@fontsource-variable/nunito": "5.2.7", "@fontsource/space-mono": "5.2.9", "@noble/hashes": "^2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c53fb51dfe..1d4e55ab0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@choochmeque/tauri-plugin-notifications-api': specifier: 0.5.0-rc.11 version: 0.5.0-rc.11 + '@choochmeque/tauri-plugin-sharekit-api': + specifier: 0.4.0-rc.5 + version: 0.4.0-rc.5 '@fontsource-variable/nunito': specifier: 5.2.7 version: 5.2.7 @@ -929,6 +932,9 @@ packages: '@choochmeque/tauri-plugin-notifications-api@0.5.0-rc.11': resolution: {integrity: sha512-TSuf8HQ4Ayp9IKKB5v2yTdgKMa/I0bqbcACj0IZ46CSpzn/HKner/BKG7rM8tGOarEcyBd/VXnDhK5cJA/YMtw==} + '@choochmeque/tauri-plugin-sharekit-api@0.4.0-rc.5': + resolution: {integrity: sha512-VpO3Uw5ZmPa0M0ta/oS5WyiW/jdNAPFh1zXbHA5dJtLMeA4RJqqj4vAlVMYoYRqtrE/1Op/GQ3jXvxgU4WgSVA==} + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -6172,6 +6178,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 + '@choochmeque/tauri-plugin-sharekit-api@0.4.0-rc.5': + dependencies: + '@tauri-apps/api': 2.11.1 + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260630.1)': diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cbf12d9301..dea4d8f788 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3754,9 +3754,15 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.1", "block2", + "libc", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", "objc2-foundation", "objc2-quartz-core", ] @@ -3787,6 +3793,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -3800,6 +3807,7 @@ dependencies = [ "bitflags 2.11.1", "block2", "dispatch2", + "libc", "objc2", ] @@ -3856,8 +3864,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.11.1", + "objc2", "objc2-core-foundation", "objc2-core-graphics", + "objc2-io-surface", ] [[package]] @@ -5269,6 +5279,7 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-os", "tauri-plugin-process", + "tauri-plugin-sharekit", "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-updater", @@ -6548,6 +6559,27 @@ dependencies = [ "tauri-plugin", ] +[[package]] +name = "tauri-plugin-sharekit" +version = "0.4.0-rc.5" +source = "git+https://github.com/Choochmeque/tauri-plugin-sharekit#9f2b4c5d8a4f0ab910d900ba234ed8bae0ab854b" +dependencies = [ + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "windows", + "windows-collections", +] + [[package]] name = "tauri-plugin-single-instance" version = "2.4.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f932110037..dfb974ee4a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -101,6 +101,7 @@ tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugi "push-notifications", ] } tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } +tauri-plugin-sharekit = { git = "https://github.com/Choochmeque/tauri-plugin-sharekit" } [target.'cfg(target_os = "ios")'.dependencies] objc2 = "0.6" diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json index f1c5c554a1..ef1d88ae14 100644 --- a/src-tauri/capabilities/mobile.json +++ b/src-tauri/capabilities/mobile.json @@ -8,6 +8,7 @@ "core:window:allow-set-badge-count", "deep-link:default", "edge-to-edge:default", - "notifications:default" + "notifications:default", + "sharekit:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db2a7a72b2..03fb09cab8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -260,7 +260,9 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()); #[cfg(mobile)] - let builder = builder.plugin(tauri_plugin_edge_to_edge::init()); + let builder = builder + .plugin(tauri_plugin_edge_to_edge::init()) + .plugin(tauri_plugin_sharekit::init()); #[cfg(target_os = "android")] let builder = builder.plugin(tauri_plugin_android_fs::init()); diff --git a/src/app/components/Modal500.test.tsx b/src/app/components/Modal500.test.tsx index ab45221429..4827d1731b 100644 --- a/src/app/components/Modal500.test.tsx +++ b/src/app/components/Modal500.test.tsx @@ -1,14 +1,17 @@ import { render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; +import { ScreenSize, ScreenSizeProvider } from '$hooks/useScreenSize'; import { Modal500 } from './Modal500'; describe('Modal500', () => { it('does not throw when rendered without tabbable children', () => { expect(() => render( - void>()}> -
Empty modal content
-
+ + void>()}> +
Empty modal content
+
+
) ).not.toThrow(); }); diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx index 042e97eb6d..7ecf4dc482 100644 --- a/src/app/components/Modal500.tsx +++ b/src/app/components/Modal500.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import { useRef } from 'react'; import FocusTrap from 'focus-trap-react'; import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds'; +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { stopPropagation } from '$utils/keyboard'; type Modal500Props = { @@ -10,6 +11,17 @@ type Modal500Props = { }; export function Modal500({ requestClose, children }: Modal500Props) { const modalRef = useRef(null); + const screenSize = useScreenSizeContext(); + + if (screenSize === ScreenSize.Mobile) { + return ( + +
+ {children} +
+
+ ); + } return ( }> diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx index 8c5879cba0..1a222f9169 100644 --- a/src/app/components/page/Page.tsx +++ b/src/app/components/page/Page.tsx @@ -13,14 +13,15 @@ type PageRootProps = { rail?: ReactNode; bottomNav?: ReactNode; children: ReactNode; + mobileDrawer?: boolean; }; -export function PageRoot({ nav, rail, bottomNav, children }: PageRootProps) { +export function PageRoot({ nav, rail, bottomNav, children, mobileDrawer = true }: PageRootProps) { const screenSize = useScreenSizeContext(); const [mobileGestures] = useSetting(settingsAtom, 'mobileGestures'); const isMobile = screenSize === ScreenSize.Mobile; - if (isMobile && mobileGestures) { + if (isMobile && mobileGestures && mobileDrawer) { return ( diff --git a/src/app/components/page/SettingsSectionPage.tsx b/src/app/components/page/SettingsSectionPage.tsx new file mode 100644 index 0000000000..84c46860fd --- /dev/null +++ b/src/app/components/page/SettingsSectionPage.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from 'react'; +import { Box, IconButton, Text } from 'folds'; +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { ArrowLeft, composerIcon, X } from '$components/icons/phosphor'; +import { Page, PageHeader } from './Page'; +import { SettingsSectionBody, SettingsSectionHeader } from './style.css'; + +type SettingsSectionPageProps = { + title: ReactNode; + requestBack?: () => void; + requestClose: () => void; + titleAs?: 'h1' | 'h2' | 'h3' | 'span' | 'div'; + backLabel?: string; + actionLabel?: string; + children?: ReactNode; +}; + +export function SettingsSectionPage({ + title, + requestBack, + requestClose, + titleAs, + backLabel, + actionLabel, + children, +}: SettingsSectionPageProps) { + const screenSize = useScreenSizeContext(); + const closeLabel = actionLabel ?? 'Close'; + const showBack = screenSize === ScreenSize.Mobile && requestBack; + + return ( + + + + + {showBack && ( + + {composerIcon(ArrowLeft)} + + )} + + {title} + + + + + {composerIcon(X)} + + + + + + {children} + + + ); +} diff --git a/src/app/components/page/index.tsx b/src/app/components/page/index.tsx index d9925d7520..387720781c 100644 --- a/src/app/components/page/index.tsx +++ b/src/app/components/page/index.tsx @@ -1 +1,2 @@ export * from './Page'; +export * from './SettingsSectionPage'; diff --git a/src/app/components/page/style.css.ts b/src/app/components/page/style.css.ts index a3add10e79..5076161090 100644 --- a/src/app/components/page/style.css.ts +++ b/src/app/components/page/style.css.ts @@ -1,4 +1,4 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; import { recipe } from '@vanilla-extract/recipes'; import { DefaultReset, color, config, toRem } from 'folds'; @@ -123,3 +123,21 @@ export const PageContentCenter = style([ margin: 'auto', }, ]); + +export const SettingsSectionBody = style({ + width: '100%', + maxWidth: toRem(800), + marginInline: 'auto', +}); + +globalStyle(`${SettingsSectionBody} *`, { + scrollbarWidth: 'none', +}); +globalStyle(`${SettingsSectionBody} *::-webkit-scrollbar`, { + display: 'none', +}); + +export const SettingsSectionHeader = style({ + paddingLeft: config.space.S300, + paddingRight: config.space.S200, +}); diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index a3a40d411c..e67b02e642 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -35,6 +35,7 @@ import { getMxIdServer } from '$utils/mxIdHelper'; import { useCloseUserRoomProfile } from '$state/hooks/userRoomProfile'; import { stopPropagation } from '$utils/keyboard'; import { copyToClipboard } from '$utils/dom'; +import { shareText } from '$utils/share'; import { getExploreServerPath } from '$pages/pathUtils'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { factoryRoomIdByAtoZ } from '$utils/sort'; @@ -300,6 +301,26 @@ export function ShareChip({ > Copy User Link + { + shareText(getMatrixToUser(userId)); + setCopied(); + close(); + }} + > + Share User Link + diff --git a/src/app/features/bug-report/BugReportModal.tsx b/src/app/features/bug-report/BugReportModal.tsx index 1305c1144e..5d4bde2443 100644 --- a/src/app/features/bug-report/BugReportModal.tsx +++ b/src/app/features/bug-report/BugReportModal.tsx @@ -85,8 +85,7 @@ export function buildGitHubUrl( return `https://github.com/${GITHUB_REPO}/issues/new?${new URLSearchParams(params)}`; } -function BugReportModal() { - const close = useCloseBugReportModal(); +export function BugReportForm({ onDone }: { onDone: () => void }) { const sentryEnabled = Sentry.isInitialized(); const [type, setType] = useState('bug'); const [title, setTitle] = useState(''); @@ -219,298 +218,299 @@ function BugReportModal() { const url = buildGitHubUrl(type, title.trim(), fields); window.open(url, '_blank', 'noopener,noreferrer'); } - close(); + onDone(); }; return ( - }> - - +
+ + Report an Issue + + + {composerIcon(X)} + +
+ + - - -
+ Type + + setType('bug')} + > + Bug Report + + setType('feature')} > - - Report an Issue + Feature Request + + + + + {/* Title + duplicate check */} + + Title * + setTitle((e.target as HTMLInputElement).value)} + /> + {searching && ( + + + Searching for similar issues… + + )} + {!searching && similarIssues.length > 0 && ( + + Similar open issues — please check before submitting: + {similarIssues.map((issue) => ( + + {'→ '} + + #{issue.number}: {issue.title} + + + ))} + + )} + + + {/* Description */} + + + {type === 'bug' ? 'Describe the bug *' : 'Describe the problem *'} + +