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..20b6b129ff 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?rev=9f2b4c5d8a4f0ab910d900ba234ed8bae0ab854b#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..d57c527ede 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", rev = "9f2b4c5d8a4f0ab910d900ba234ed8bae0ab854b" } [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..6dbd04c715 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,29 @@ 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..c2709a9b3c 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 + { + const shared = await shareText(getMatrixToUser(userId)); + if (shared) 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 *'} + +