diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5ef7cb37f8ab6..894f535ebcd8a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -48,7 +48,7 @@ "generate:highlights": "node scripts/generate-highlights.mts", "generate:nitro": "cd modules/t3-highlights && nitrogen", "eas-build-on-success": "node scripts/upload-sourcemaps.mts build", - "test": "vp test run --project unit --project react && T3_MOBILE_TEST_PLATFORM=android vp test run --project react src/components/FilePreviewModal.test.tsx", + "test": "vp test run --project unit --project react", "typecheck": "effect-tsgo tsrs --noEmit" }, "dependencies": { diff --git a/apps/mobile/src/components/ComposerAttachmentStrip.tsx b/apps/mobile/src/components/ComposerAttachmentStrip.tsx index 923afb87a3a1f..49d30e06e45f3 100644 --- a/apps/mobile/src/components/ComposerAttachmentStrip.tsx +++ b/apps/mobile/src/components/ComposerAttachmentStrip.tsx @@ -1,4 +1,4 @@ -import { FilePreviewGallery, useFilePreviewGallery } from "./FilePreviewGallery"; +import { FilePreviewGallery, useFilePreviewGallery } from "./ui/FilePreviewGallery"; import { composerImageGallery } from "../lib/composerImageGallery"; import { SymbolView } from "./ui/AppSymbol"; import { imageMimeType } from "@t3tools/shared/image"; @@ -21,7 +21,7 @@ import { resolveOwnedComposerAttachmentFileUri } from "../lib/composerAttachment import { VideoAttachmentTile } from "./VideoAttachmentTile"; import { type MediaActionsSource } from "../lib/mediaActionsSource"; import { PresentationSource } from "./ui/NativePresentation"; -import type { FilePreviewSource } from "./FilePreviewModal"; +import type { FilePreviewSource } from "./ui/FilePreviewModal"; import { isPdfFile } from "../lib/filePreview"; import type { EnvironmentId } from "@t3tools/contracts"; import { diff --git a/apps/mobile/src/components/ComposerContextAttachment.tsx b/apps/mobile/src/components/ComposerContextAttachment.tsx index edb5522f3d0f8..fcd0da2b9d789 100644 --- a/apps/mobile/src/components/ComposerContextAttachment.tsx +++ b/apps/mobile/src/components/ComposerContextAttachment.tsx @@ -16,7 +16,7 @@ import { downloadAndShareAttachment } from "../lib/attachmentDownload"; import { assetImageSource } from "../lib/assetImageSource"; import { useAssetUrlState, useRefreshAssetUrl } from "../state/assets"; import { AppText as Text } from "./ui/AppText"; -import { FilePreviewModal } from "./FilePreviewModal"; +import { FilePreviewModal } from "./ui/FilePreviewModal"; import { PresentationSource } from "./ui/NativePresentation"; export function ComposerContextAttachment(props: { diff --git a/apps/mobile/src/components/ComposerContextSheet.tsx b/apps/mobile/src/components/ComposerContextSheet.tsx index d7fb3a3c4cd14..8836662df1ff0 100644 --- a/apps/mobile/src/components/ComposerContextSheet.tsx +++ b/apps/mobile/src/components/ComposerContextSheet.tsx @@ -23,8 +23,8 @@ import { type DraftComposerAttachment, } from "../lib/composerImages"; import { contextImageGallery } from "../lib/contextImageGallery"; -import { FilePreviewModal } from "./FilePreviewModal"; -import { VideoPreviewModal } from "./VideoPreviewModal"; +import { FilePreviewModal } from "./ui/FilePreviewModal"; +import { VideoPreviewModal } from "./ui/VideoPreviewModal"; import { ComposerContextAttachment } from "./ComposerContextAttachment"; import { AppText as Text } from "./ui/AppText"; import { Sheet } from "./ui/Sheet"; diff --git a/apps/mobile/src/components/FilePreviewModal.test.tsx b/apps/mobile/src/components/FilePreviewModal.test.tsx deleted file mode 100644 index 3989180f62824..0000000000000 --- a/apps/mobile/src/components/FilePreviewModal.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import { expect, it, vi } from "vite-plus/test"; -import { EnvironmentId, SOURCE_CONTROL_MEDIA_ASSET } from "@t3tools/contracts"; - -const harness = vi.hoisted(() => ({ - refreshAssetUrl: vi.fn<(environmentId: string, resource: { url: string }) => Promise>(), - alert: vi.fn(), - previews: [] as unknown[], - mounts: 0, - platform: { OS: process.env.T3_MOBILE_TEST_PLATFORM === "android" ? "android" : "ios" }, -})); - -vi.mock("react-native", () => ({ Platform: harness.platform })); -vi.mock("../state/assets", () => ({ refreshAssetUrl: harness.refreshAssetUrl })); -vi.mock("../lib/localAttachmentPreview", () => ({ loadLocalAttachmentPreview: vi.fn() })); -vi.mock("../lib/attachmentDownload", () => ({ - downloadAndShareAttachment: vi.fn(), - nativeViewerErrorMessage: () => "", - shareLocalAttachment: vi.fn(), -})); -vi.mock("../lib/previewPresentation", () => ({ usePreviewPresentation: () => {} })); -vi.mock("./ui/Dialog", () => ({ alert: harness.alert })); -vi.mock("./FilePreview", async () => { - const { useState } = await import("react"); - return { - FilePreview: (props: { source: unknown }) => { - useState(() => harness.mounts++); - harness.previews.push(props.source); - return null; - }, - }; -}); - -import type { FilePreviewItem } from "./FilePreviewModal.types"; -import { FilePreviewModal } from "./FilePreviewModal"; - -const environmentId = EnvironmentId.make("environment"); -const image = (url: string): FilePreviewItem => ({ - kind: "image", - environmentId, - resource: { _tag: SOURCE_CONTROL_MEDIA_ASSET, cwd: "/repo", url }, -}); - -async function openGallery( - selected: FilePreviewItem, - gallery: readonly FilePreviewItem[], - platform = harness.platform.OS, -) { - harness.platform.OS = platform; - (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - harness.previews.length = 0; - harness.mounts = 0; - harness.alert.mockClear(); - const root = createRoot(document.createElement("div")); - const onRequestClose = vi.fn(); - await act(async () => - root.render( - , - ), - ); - return { onRequestClose, [Symbol.asyncDispose]: () => act(async () => root.unmount()) }; -} - -it("keeps the gallery stable while a sibling is still resolving", async () => { - const platform = harness.platform.OS; - const slow = Promise.withResolvers(); - harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => - resource.url.endsWith("slow.png") ? slow.promise : "https://signed.test/a.png", - ); - const selected = image("https://github.com/a.png"); - await using _preview = await openGallery( - selected, - [image("https://github.com/slow.png"), selected], - platform, - ); - - if (platform === "ios") { - expect(harness.previews.at(-1)).toMatchObject({ - kind: "image", - selectedIndex: 1, - images: [ - expect.not.objectContaining({ uri: expect.anything() }), - { uri: "https://signed.test/a.png" }, - ], - }); - } else { - expect(harness.previews).toEqual([]); - } - await act(async () => slow.resolve("https://signed.test/slow.png")); - expect(harness.previews.at(-1)).toMatchObject({ - selectedIndex: 1, - images: [{ uri: "https://signed.test/slow.png" }, { uri: "https://signed.test/a.png" }], - }); - expect(harness.mounts).toBe(1); -}); - -it("handles a failed sibling without leaving a pending page", async () => { - const platform = harness.platform.OS; - harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => { - if (resource.url.endsWith("missing.png")) throw new Error("not found"); - return `https://signed.test/${resource.url.split("/").pop()}`; - }); - const selected = image("https://github.com/a.png"); - await using preview = await openGallery( - selected, - [selected, image("https://github.com/missing.png"), image("https://github.com/c.png")], - platform, - ); - - if (platform === "android") { - expect(preview.onRequestClose).toHaveBeenCalledOnce(); - expect(harness.alert).toHaveBeenCalledOnce(); - expect(harness.previews).toEqual([]); - return; - } - - expect(preview.onRequestClose).not.toHaveBeenCalled(); - expect(harness.alert).not.toHaveBeenCalled(); - expect(harness.previews.at(-1)).toMatchObject({ - selectedIndex: 0, - images: [ - { uri: "https://signed.test/a.png" }, - { failed: true }, - { uri: "https://signed.test/c.png" }, - ], - }); -}); - -it("closes the preview when the selected image cannot be resolved", async () => { - harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => { - if (resource.url.endsWith("missing.png")) throw new Error("not found"); - return `https://signed.test/${resource.url.split("/").pop()}`; - }); - const selected = image("https://github.com/missing.png"); - await using preview = await openGallery(selected, [image("https://github.com/a.png"), selected]); - - expect(preview.onRequestClose).toHaveBeenCalledOnce(); - expect(harness.alert).toHaveBeenCalledOnce(); -}); diff --git a/apps/mobile/src/components/MediaImagePreview.tsx b/apps/mobile/src/components/MediaImagePreview.tsx deleted file mode 100644 index 2a7b226574e73..0000000000000 --- a/apps/mobile/src/components/MediaImagePreview.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { createContext, useContext } from "react"; -import { View } from "react-native"; -import ImageViewing from "react-native-image-viewing"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -import { useMediaActions } from "../state/mediaActions"; -import type { FilePreviewImages } from "./FilePreviewModal.types"; -import { MediaPreviewHeader } from "./MediaPreviewHeader"; - -type MediaImagePreviewProps = { - readonly source: FilePreviewImages; - readonly onRequestClose: () => void; -}; - -const ImagePreviewContext = createContext(null); - -function ImagePreviewHeader(header: { readonly imageIndex: number }) { - const props = useContext(ImagePreviewContext)!; - const source = props.source.images[header.imageIndex]; - const insets = useSafeAreaInsets(); - const mediaActions = useMediaActions(source?.actionsSource, props.onRequestClose); - return ( - - - - ); -} - -// Images still resolving show the viewer's loading page, and remount by key once their URL arrives. -export function imageViewingProps(source: FilePreviewImages) { - return { - images: source.images.map((image) => (image.uri ? { uri: image.uri } : {})), - keyExtractor: (_: unknown, index: number) => source.images[index]?.uri ?? `pending:${index}`, - // Stays fixed: react-native-image-viewing remounts the Modal and replays its fade when it changes. - imageIndex: source.selectedIndex, - }; -} - -export function MediaImagePreview(props: MediaImagePreviewProps) { - return ( - - - - ); -} diff --git a/apps/mobile/src/components/ui/AndroidAnchoredMenu.tsx b/apps/mobile/src/components/ui/AndroidAnchoredMenu.android.tsx similarity index 100% rename from apps/mobile/src/components/ui/AndroidAnchoredMenu.tsx rename to apps/mobile/src/components/ui/AndroidAnchoredMenu.android.tsx diff --git a/apps/mobile/src/components/ui/AndroidScreenHeader.tsx b/apps/mobile/src/components/ui/AndroidScreenHeader.android.tsx similarity index 99% rename from apps/mobile/src/components/ui/AndroidScreenHeader.tsx rename to apps/mobile/src/components/ui/AndroidScreenHeader.android.tsx index b7ab699b11b57..cee8acef23bae 100644 --- a/apps/mobile/src/components/ui/AndroidScreenHeader.tsx +++ b/apps/mobile/src/components/ui/AndroidScreenHeader.android.tsx @@ -7,7 +7,7 @@ import { View } from "react-native"; import type { AppSymbolName } from "./AppSymbol"; import { NativeAppBar } from "./NativeAppBar.android"; import { IconButton } from "./IconButton"; -import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu"; +import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu.android"; import { splitAndroidHeaderActions } from "./headerMenu.android"; import { useScaledTextRole } from "../../features/settings/appearance/useScaledTextRole"; import { useToolbarLayout } from "./useToolbarLayout"; diff --git a/apps/mobile/src/components/ui/AppBar.android.test.tsx b/apps/mobile/src/components/ui/AppBar.android.test.tsx index 12859f11dfe1a..4aa9c755450f5 100644 --- a/apps/mobile/src/components/ui/AppBar.android.test.tsx +++ b/apps/mobile/src/components/ui/AppBar.android.test.tsx @@ -13,7 +13,7 @@ vi.mock("@react-navigation/native", () => ({ throw new Error("Couldn't find a navigation object."); }, })); -vi.mock("./AndroidScreenHeader", () => ({ +vi.mock("./AndroidScreenHeader.android", () => ({ AndroidScreenHeader: (props: { readonly title: string }) =>

{props.title}

, })); vi.mock("./WorkspaceAppBar.android", () => ({ diff --git a/apps/mobile/src/components/ui/AppBar.android.tsx b/apps/mobile/src/components/ui/AppBar.android.tsx index 9708048704235..6667f06fb08da 100644 --- a/apps/mobile/src/components/ui/AppBar.android.tsx +++ b/apps/mobile/src/components/ui/AppBar.android.tsx @@ -8,7 +8,7 @@ import { useSidebarToggle } from "../../features/layout/useSidebarToggle"; import { useAdaptiveWorkspaceLayout } from "../../features/layout/adaptive-workspace-context"; import { InspectorCloseContext } from "../../features/layout/inspector-bar-store"; import { useAppearancePreferences } from "../../features/settings/appearance/AppearancePreferencesProvider"; -import { AndroidHeaderIconButton, AndroidScreenHeader } from "./AndroidScreenHeader"; +import { AndroidHeaderIconButton, AndroidScreenHeader } from "./AndroidScreenHeader.android"; import { ScreenHeaderButton } from "./ScreenHeaderButton.android"; import { Menu } from "./Menu"; import { Button } from "./Button"; diff --git a/apps/mobile/src/components/ui/AppSymbol.android.tsx b/apps/mobile/src/components/ui/AppSymbol.android.tsx new file mode 100644 index 0000000000000..47d199110cf4c --- /dev/null +++ b/apps/mobile/src/components/ui/AppSymbol.android.tsx @@ -0,0 +1,370 @@ +import type { Icon } from "@tabler/icons-react-native/types"; +/* Package-root imports eagerly register the entire Tabler icon set in Metro. */ +import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal"; +import IconAlignLeft from "@tabler/icons-react-native/IconAlignLeft"; +import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle"; +import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle"; +import IconApps from "@tabler/icons-react-native/IconApps"; +import IconArchive from "@tabler/icons-react-native/IconArchive"; +import IconCalendarClock from "@tabler/icons-react-native/IconCalendarClock"; +import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp"; +import IconArrowDown from "@tabler/icons-react-native/IconArrowDown"; +import IconArrowForwardUp from "@tabler/icons-react-native/IconArrowForwardUp"; +import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft"; +import IconArrowRight from "@tabler/icons-react-native/IconArrowRight"; +import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle"; +import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle"; +import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; +import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; +import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; +import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; +import IconArrowsLeftRight from "@tabler/icons-react-native/IconArrowsLeftRight"; +import IconArrowsDiagonal2 from "@tabler/icons-react-native/IconArrowsDiagonal2"; +import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize"; +import IconBan from "@tabler/icons-react-native/IconBan"; +import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; +import IconBolt from "@tabler/icons-react-native/IconBolt"; +import IconBox from "@tabler/icons-react-native/IconBox"; +import IconBrain from "@tabler/icons-react-native/IconBrain"; +import IconCamera from "@tabler/icons-react-native/IconCamera"; +import IconCar from "@tabler/icons-react-native/IconCar"; +import IconChartBar from "@tabler/icons-react-native/IconChartBar"; +import IconCheck from "@tabler/icons-react-native/IconCheck"; +import IconCloud from "@tabler/icons-react-native/IconCloud"; +import IconChevronDown from "@tabler/icons-react-native/IconChevronDown"; +import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft"; +import IconChevronRight from "@tabler/icons-react-native/IconChevronRight"; +import IconChevronUp from "@tabler/icons-react-native/IconChevronUp"; +import IconCircle from "@tabler/icons-react-native/IconCircle"; +import IconCircleFilled from "@tabler/icons-react-native/IconCircleFilled"; +import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck"; +import IconCircleDashed from "@tabler/icons-react-native/IconCircleDashed"; +import IconCircleDot from "@tabler/icons-react-native/IconCircleDot"; +import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled"; +import IconMoodPlus from "@tabler/icons-react-native/IconMoodPlus"; +import IconTag from "@tabler/icons-react-native/IconTag"; +import IconTicket from "@tabler/icons-react-native/IconTicket"; +import IconClock from "@tabler/icons-react-native/IconClock"; +import IconCode from "@tabler/icons-react-native/IconCode"; +import IconCopy from "@tabler/icons-react-native/IconCopy"; +import IconDatabase from "@tabler/icons-react-native/IconDatabase"; +import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop"; +import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop"; +import IconDeviceMobile from "@tabler/icons-react-native/IconDeviceMobile"; +import IconDots from "@tabler/icons-react-native/IconDots"; +import IconPlusMinus from "@tabler/icons-react-native/IconPlusMinus"; +import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical"; +import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal"; +import IconEdit from "@tabler/icons-react-native/IconEdit"; +import IconExternalLink from "@tabler/icons-react-native/IconExternalLink"; +import IconEyeOff from "@tabler/icons-react-native/IconEyeOff"; +import IconEye from "@tabler/icons-react-native/IconEye"; +import IconFileText from "@tabler/icons-react-native/IconFileText"; +import IconFileDiff from "@tabler/icons-react-native/IconFileDiff"; +import IconFilter from "@tabler/icons-react-native/IconFilter"; +import IconFilterFilled from "@tabler/icons-react-native/IconFilterFilled"; +import IconFolder from "@tabler/icons-react-native/IconFolder"; +import IconFolderOpen from "@tabler/icons-react-native/IconFolderOpen"; +import IconHeadphones from "@tabler/icons-react-native/IconHeadphones"; +import IconFolderPlus from "@tabler/icons-react-native/IconFolderPlus"; +import IconGitPullRequestClosed from "@tabler/icons-react-native/IconGitPullRequestClosed"; +import IconGitPullRequestDraft from "@tabler/icons-react-native/IconGitPullRequestDraft"; +import IconGitCommit from "@tabler/icons-react-native/IconGitCommit"; +import IconGitBranch from "@tabler/icons-react-native/IconGitBranch"; +import IconGitMerge from "@tabler/icons-react-native/IconGitMerge"; +import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest"; +import IconHammer from "@tabler/icons-react-native/IconHammer"; +import IconHome from "@tabler/icons-react-native/IconHome"; +import IconInfoCircle from "@tabler/icons-react-native/IconInfoCircle"; +import IconKeyboard from "@tabler/icons-react-native/IconKeyboard"; +import IconKeyboardHide from "@tabler/icons-react-native/IconKeyboardHide"; +import IconLock from "@tabler/icons-react-native/IconLock"; +import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns"; +import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar"; +import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight"; +import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing"; +import IconMicrophone from "@tabler/icons-react-native/IconMicrophone"; +import IconLink from "@tabler/icons-react-native/IconLink"; +import IconListNumbers from "@tabler/icons-react-native/IconListNumbers"; +import IconMenu2 from "@tabler/icons-react-native/IconMenu2"; +import IconMessageCircleQuestion from "@tabler/icons-react-native/IconMessageCircleQuestion"; +import IconMessage from "@tabler/icons-react-native/IconMessage"; +import IconMessagePlus from "@tabler/icons-react-native/IconMessagePlus"; +import IconMinus from "@tabler/icons-react-native/IconMinus"; +import IconMoon from "@tabler/icons-react-native/IconMoon"; +import IconNetwork from "@tabler/icons-react-native/IconNetwork"; +import IconPalette from "@tabler/icons-react-native/IconPalette"; +import IconPencil from "@tabler/icons-react-native/IconPencil"; +import IconPlug from "@tabler/icons-react-native/IconPlug"; +import IconPhoto from "@tabler/icons-react-native/IconPhoto"; +import IconPin from "@tabler/icons-react-native/IconPin"; +import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff"; +import IconPlayerPause from "@tabler/icons-react-native/IconPlayerPause"; +import IconPlayerPlay from "@tabler/icons-react-native/IconPlayerPlay"; +import IconPlayerStopFilled from "@tabler/icons-react-native/IconPlayerStopFilled"; +import IconPlus from "@tabler/icons-react-native/IconPlus"; +import IconQrcode from "@tabler/icons-react-native/IconQrcode"; +import IconRefresh from "@tabler/icons-react-native/IconRefresh"; +import IconSearch from "@tabler/icons-react-native/IconSearch"; +import IconShare from "@tabler/icons-react-native/IconShare"; +import IconServer from "@tabler/icons-react-native/IconServer"; +import IconSettings from "@tabler/icons-react-native/IconSettings"; +import IconSparkles from "@tabler/icons-react-native/IconSparkles"; +import IconStack2 from "@tabler/icons-react-native/IconStack2"; +import IconStar from "@tabler/icons-react-native/IconStar"; +import IconStarFilled from "@tabler/icons-react-native/IconStarFilled"; +import IconStethoscope from "@tabler/icons-react-native/IconStethoscope"; +import IconSun from "@tabler/icons-react-native/IconSun"; +import IconShieldQuestion from "@tabler/icons-react-native/IconShieldQuestion"; +import IconTarget from "@tabler/icons-react-native/IconTarget"; +import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2"; +import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease"; +import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease"; +import IconTool from "@tabler/icons-react-native/IconTool"; +import IconTrash from "@tabler/icons-react-native/IconTrash"; +import IconTypography from "@tabler/icons-react-native/IconTypography"; +import IconDownload from "@tabler/icons-react-native/IconDownload"; +import IconSitemap from "@tabler/icons-react-native/IconSitemap"; +import IconUpload from "@tabler/icons-react-native/IconUpload"; +import IconUserCircle from "@tabler/icons-react-native/IconUserCircle"; +import IconUserCheck from "@tabler/icons-react-native/IconUserCheck"; +import IconUserX from "@tabler/icons-react-native/IconUserX"; +import IconUserQuestion from "@tabler/icons-react-native/IconUserQuestion"; +import IconUserPlus from "@tabler/icons-react-native/IconUserPlus"; +import IconUsers from "@tabler/icons-react-native/IconUsers"; +import IconWifi from "@tabler/icons-react-native/IconWifi"; +import IconWifiOff from "@tabler/icons-react-native/IconWifiOff"; +import IconWorld from "@tabler/icons-react-native/IconWorld"; +import IconX from "@tabler/icons-react-native/IconX"; +import type { AndroidSymbol, SFSymbol, SymbolViewProps } from "expo-symbols"; +import { withUniwind } from "uniwind"; + +const ANDROID_ICON_BY_SF_SYMBOL = { + plusminus: IconPlusMinus, + "git.pull-request.closed": IconGitPullRequestClosed, + "git.pull-request.draft": IconGitPullRequestDraft, + "git.commit": IconGitCommit, + "arrow.branch": IconGitBranch, + "arrow.left": IconArrowLeft, + "arrow.right": IconArrowRight, + "arrow.left.arrow.right": IconArrowsLeftRight, + "arrow.clockwise": IconRefresh, + "arrow.down": IconArrowDown, + "arrow.down.circle": IconArrowDownCircle, + "arrow.right.circle": IconArrowRightCircle, + "arrow.triangle.branch": IconGitBranch, + "arrow.triangle.merge": IconGitMerge, + "arrow.triangle.pull": IconGitPullRequest, + "square.3.layers.3d": IconStack2, + "arrow.turn.left.up": IconArrowBackUp, + "arrow.up": IconArrowUp, + "arrow.up.circle": IconArrowUpCircle, + "arrow.up.left.and.arrow.down.right": IconArrowsDiagonal2, + "arrow.down.right.and.arrow.up.left": IconArrowsMinimize, + "arrow.up.right": IconArrowUpRight, + "arrow.up.right.circle": IconArrowUpRightCircle, + "arrow.uturn.backward": IconArrowBackUp, + "arrow.uturn.forward": IconArrowForwardUp, + archivebox: IconArchive, + "archivebox.fill": IconArchive, + "bell.badge": IconBellRinging, + "bolt.circle": IconBolt, + "bolt.horizontal.circle": IconBolt, + brain: IconBrain, + "cable.connector": IconPlug, + camera: IconCamera, + car: IconCar, + "chart.bar.xaxis": IconChartBar, + checkmark: IconCheck, + "checkmark.circle": IconCircleCheck, + "checkmark.circle.fill": IconCircleCheck, + circle: IconCircle, + "circle.fill": IconCircleFilled, + "circle.dashed": IconCircleDashed, + "circle.inset.filled": IconCircleDot, + "circle.dashed.inset.filled": IconCircleDot, + "calendar.badge.clock": IconCalendarClock, + clock: IconClock, + timer: IconClock, + "face.smiling": IconMoodPlus, + tag: IconTag, + ticket: IconTicket, + cloud: IconCloud, + cube: IconBox, + "chevron.down": IconChevronDown, + "chevron.left": IconChevronLeft, + "chevron.left.forwardslash.chevron.right": IconCode, + "chevron.right": IconChevronRight, + "chevron.up": IconChevronUp, + desktopcomputer: IconDeviceDesktop, + doc: IconFileText, + "doc.on.doc": IconCopy, + "doc.text": IconFileText, + "file.diff": IconFileDiff, + ellipsis: IconDots, + moon: IconMoon, + "ellipsis.circle": IconDotsCircleHorizontal, + "exclamationmark.triangle": IconAlertTriangle, + "exclamationmark.circle": IconAlertCircle, + eye: IconEye, + "eye.slash": IconEyeOff, + folder: IconFolder, + "folder.badge.plus": IconFolderPlus, + "folder.fill": IconFolder, + gearshape: IconSettings, + globe: IconWorld, + hammer: IconHammer, + headphones: IconHeadphones, + house: IconHome, + "info.circle": IconInfoCircle, + internaldrive: IconDatabase, + iphone: IconDeviceMobile, + keyboard: IconKeyboard, + laptopcomputer: IconDeviceLaptop, + link: IconLink, + "list.number": IconListNumbers, + "line.3.horizontal": IconMenu2, + "line.3.horizontal.decrease": IconFilter, + "line.3.horizontal.decrease.circle": IconFilter, + "line.3.horizontal.decrease.circle.fill": IconFilterFilled, + macmini: IconServer, + macstudio: IconDeviceDesktop, + magnifyingglass: IconSearch, + mic: IconMicrophone, + paintbrush: IconPalette, + pencil: IconPencil, + "person.crop.circle": IconUserCircle, + "person.2": IconUsers, + "person.fill.checkmark": IconUserCheck, + "person.fill.xmark": IconUserX, + "person.badge.clock": IconUserQuestion, + "person.badge.plus": IconUserPlus, + photo: IconPhoto, + pin: IconPin, + "pin.slash": IconPinnedOff, + play: IconPlayerPlay, + pause: IconPlayerPause, + plus: IconPlus, + minus: IconMinus, + "slash.circle": IconBan, + "qrcode.viewfinder": IconQrcode, + "point.3.connected.trianglepath.dotted": IconNetwork, + flowchart: IconSitemap, + "point.topleft.down.curvedto.point.bottomright.up": IconGitMerge, + safari: IconExternalLink, + "server.rack": IconServer, + stethoscope: IconStethoscope, + "sidebar.left": IconLayoutSidebar, + "sidebar.right": IconLayoutSidebarRight, + "slider.horizontal.3": IconAdjustmentsHorizontal, + "square.and.arrow.up": IconShare, + "square.and.pencil": IconEdit, + "square.on.square": IconCopy, + "square.grid.2x2": IconApps, + "square.split.2x1": IconLayoutColumns, + star: IconStar, + "star.fill": IconStarFilled, + "sun.max": IconSun, + "stop.fill": IconPlayerStopFilled, + target: IconTarget, + terminal: IconTerminal2, + "text.alignleft": IconAlignLeft, + "text.bubble": IconMessage, + "questionmark.bubble": IconMessageCircleQuestion, + "exclamationmark.shield": IconShieldQuestion, + "bubble.left.and.bubble.right": IconMessage, + "bubble.left": IconMessage, + sparkles: IconSparkles, + "plus.bubble": IconMessagePlus, + "text.word.spacing": IconLetterSpacing, + "textformat.size": IconTypography, + "textformat.size.larger": IconTextIncrease, + "textformat.size.smaller": IconTextDecrease, + "tray.and.arrow.down": IconDownload, + "tray.and.arrow.up": IconUpload, + trash: IconTrash, + wifi: IconWifi, + "wifi.slash": IconWifiOff, + xmark: IconX, + "xmark.circle.fill": IconCircleXFilled, +} satisfies Partial< + Record< + SFSymbol | "git.pull-request.closed" | "git.pull-request.draft" | "git.commit" | "file.diff", + Icon + > +>; +const SF_ICON_LOOKUP: Partial< + Record< + SFSymbol | "git.pull-request.closed" | "git.pull-request.draft" | "git.commit" | "file.diff", + Icon + > +> = ANDROID_ICON_BY_SF_SYMBOL; + +const ANDROID_ICON_BY_MATERIAL_NAME = { + auto_awesome: IconSparkles, + bolt: IconBolt, + build: IconTool, + chat: IconMessage, + chat_bubble: IconMessage, + check: IconCheck, + close: IconX, + construction: IconHammer, + content_copy: IconCopy, + desktop_windows: IconDeviceDesktop, + edit: IconEdit, + error: IconAlertCircle, + folder: IconFolder, + folder_open: IconFolderOpen, + keyboard: IconKeyboard, + keyboard_arrow_down: IconChevronDown, + keyboard_arrow_up: IconChevronUp, + keyboard_hide: IconKeyboardHide, + lock: IconLock, + more_vert: IconDotsVertical, + merge: IconGitMerge, + psychology: IconBrain, + public: IconWorld, + remove: IconMinus, + smartphone: IconDeviceMobile, + terminal: IconTerminal2, + visibility: IconEye, +} satisfies Partial>; + +export type { SFSymbol } from "expo-symbols"; +export type AppSymbolName = + | keyof typeof ANDROID_ICON_BY_SF_SYMBOL + | { + ios: SFSymbol; + android: keyof typeof ANDROID_ICON_BY_MATERIAL_NAME; + }; + +export type AppSymbolViewProps = Omit & { name: AppSymbolName }; + +export function isAppSymbolName(name: string): name is Extract { + return Object.prototype.hasOwnProperty.call(ANDROID_ICON_BY_SF_SYMBOL, name); +} + +function AppSymbolView(props: AppSymbolViewProps) { + const materialName = typeof props.name === "string" ? undefined : props.name.android; + const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; + const AndroidIcon = + (materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ?? + (sfSymbol ? SF_ICON_LOOKUP[sfSymbol] : undefined); + + if (!AndroidIcon) { + return props.fallback ?? null; + } + + return ( + + ); +} + +export const SymbolView = withUniwind(AppSymbolView); diff --git a/apps/mobile/src/components/ui/AppSymbol.ios.tsx b/apps/mobile/src/components/ui/AppSymbol.ios.tsx deleted file mode 100644 index 47b7e5be75bc3..0000000000000 --- a/apps/mobile/src/components/ui/AppSymbol.ios.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import IconGitBranch from "@tabler/icons-react-native/IconGitBranch"; -import IconGitMerge from "@tabler/icons-react-native/IconGitMerge"; -import IconGitCommit from "@tabler/icons-react-native/IconGitCommit"; -import IconGitPullRequestClosed from "@tabler/icons-react-native/IconGitPullRequestClosed"; -import IconGitPullRequestDraft from "@tabler/icons-react-native/IconGitPullRequestDraft"; -import type { SFSymbol } from "expo-symbols"; -import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest"; -import IconFileDiff from "@tabler/icons-react-native/IconFileDiff"; -import { SymbolView as ExpoSymbolView } from "expo-symbols"; -import { withUniwind } from "uniwind"; -import type { AppSymbolViewProps } from "./AppSymbol"; - -export type { SFSymbol } from "expo-symbols"; -export type { AppSymbolName } from "./AppSymbol"; - -const CUSTOM_ICONS = { - "file.diff": IconFileDiff, - "arrow.triangle.pull": IconGitPullRequest, - "arrow.triangle.merge": IconGitMerge, - "arrow.triangle.branch": IconGitBranch, - "arrow.branch": IconGitBranch, - "git.pull-request.closed": IconGitPullRequestClosed, - "git.pull-request.draft": IconGitPullRequestDraft, - "git.commit": IconGitCommit, -}; - -function AppSymbolView(props: AppSymbolViewProps) { - const name = typeof props.name === "string" ? props.name : props.name.ios; - const CustomIcon = CUSTOM_ICONS[name as keyof typeof CUSTOM_ICONS]; - if (CustomIcon) { - return ( - - ); - } - - return ; -} - -export const SymbolView = withUniwind(AppSymbolView); diff --git a/apps/mobile/src/components/ui/AppSymbol.tsx b/apps/mobile/src/components/ui/AppSymbol.tsx index 47d199110cf4c..9e4915890e75b 100644 --- a/apps/mobile/src/components/ui/AppSymbol.tsx +++ b/apps/mobile/src/components/ui/AppSymbol.tsx @@ -1,370 +1,46 @@ -import type { Icon } from "@tabler/icons-react-native/types"; -/* Package-root imports eagerly register the entire Tabler icon set in Metro. */ -import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal"; -import IconAlignLeft from "@tabler/icons-react-native/IconAlignLeft"; -import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle"; -import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle"; -import IconApps from "@tabler/icons-react-native/IconApps"; -import IconArchive from "@tabler/icons-react-native/IconArchive"; -import IconCalendarClock from "@tabler/icons-react-native/IconCalendarClock"; -import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp"; -import IconArrowDown from "@tabler/icons-react-native/IconArrowDown"; -import IconArrowForwardUp from "@tabler/icons-react-native/IconArrowForwardUp"; -import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft"; -import IconArrowRight from "@tabler/icons-react-native/IconArrowRight"; -import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle"; -import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle"; -import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; -import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; -import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; -import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; -import IconArrowsLeftRight from "@tabler/icons-react-native/IconArrowsLeftRight"; -import IconArrowsDiagonal2 from "@tabler/icons-react-native/IconArrowsDiagonal2"; -import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize"; -import IconBan from "@tabler/icons-react-native/IconBan"; -import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; -import IconBolt from "@tabler/icons-react-native/IconBolt"; -import IconBox from "@tabler/icons-react-native/IconBox"; -import IconBrain from "@tabler/icons-react-native/IconBrain"; -import IconCamera from "@tabler/icons-react-native/IconCamera"; -import IconCar from "@tabler/icons-react-native/IconCar"; -import IconChartBar from "@tabler/icons-react-native/IconChartBar"; -import IconCheck from "@tabler/icons-react-native/IconCheck"; -import IconCloud from "@tabler/icons-react-native/IconCloud"; -import IconChevronDown from "@tabler/icons-react-native/IconChevronDown"; -import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft"; -import IconChevronRight from "@tabler/icons-react-native/IconChevronRight"; -import IconChevronUp from "@tabler/icons-react-native/IconChevronUp"; -import IconCircle from "@tabler/icons-react-native/IconCircle"; -import IconCircleFilled from "@tabler/icons-react-native/IconCircleFilled"; -import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck"; -import IconCircleDashed from "@tabler/icons-react-native/IconCircleDashed"; -import IconCircleDot from "@tabler/icons-react-native/IconCircleDot"; -import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled"; -import IconMoodPlus from "@tabler/icons-react-native/IconMoodPlus"; -import IconTag from "@tabler/icons-react-native/IconTag"; -import IconTicket from "@tabler/icons-react-native/IconTicket"; -import IconClock from "@tabler/icons-react-native/IconClock"; -import IconCode from "@tabler/icons-react-native/IconCode"; -import IconCopy from "@tabler/icons-react-native/IconCopy"; -import IconDatabase from "@tabler/icons-react-native/IconDatabase"; -import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop"; -import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop"; -import IconDeviceMobile from "@tabler/icons-react-native/IconDeviceMobile"; -import IconDots from "@tabler/icons-react-native/IconDots"; -import IconPlusMinus from "@tabler/icons-react-native/IconPlusMinus"; -import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical"; -import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal"; -import IconEdit from "@tabler/icons-react-native/IconEdit"; -import IconExternalLink from "@tabler/icons-react-native/IconExternalLink"; -import IconEyeOff from "@tabler/icons-react-native/IconEyeOff"; -import IconEye from "@tabler/icons-react-native/IconEye"; -import IconFileText from "@tabler/icons-react-native/IconFileText"; -import IconFileDiff from "@tabler/icons-react-native/IconFileDiff"; -import IconFilter from "@tabler/icons-react-native/IconFilter"; -import IconFilterFilled from "@tabler/icons-react-native/IconFilterFilled"; -import IconFolder from "@tabler/icons-react-native/IconFolder"; -import IconFolderOpen from "@tabler/icons-react-native/IconFolderOpen"; -import IconHeadphones from "@tabler/icons-react-native/IconHeadphones"; -import IconFolderPlus from "@tabler/icons-react-native/IconFolderPlus"; -import IconGitPullRequestClosed from "@tabler/icons-react-native/IconGitPullRequestClosed"; -import IconGitPullRequestDraft from "@tabler/icons-react-native/IconGitPullRequestDraft"; -import IconGitCommit from "@tabler/icons-react-native/IconGitCommit"; import IconGitBranch from "@tabler/icons-react-native/IconGitBranch"; import IconGitMerge from "@tabler/icons-react-native/IconGitMerge"; +import IconGitCommit from "@tabler/icons-react-native/IconGitCommit"; +import IconGitPullRequestClosed from "@tabler/icons-react-native/IconGitPullRequestClosed"; +import IconGitPullRequestDraft from "@tabler/icons-react-native/IconGitPullRequestDraft"; +import type { SFSymbol } from "expo-symbols"; import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest"; -import IconHammer from "@tabler/icons-react-native/IconHammer"; -import IconHome from "@tabler/icons-react-native/IconHome"; -import IconInfoCircle from "@tabler/icons-react-native/IconInfoCircle"; -import IconKeyboard from "@tabler/icons-react-native/IconKeyboard"; -import IconKeyboardHide from "@tabler/icons-react-native/IconKeyboardHide"; -import IconLock from "@tabler/icons-react-native/IconLock"; -import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns"; -import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar"; -import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight"; -import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing"; -import IconMicrophone from "@tabler/icons-react-native/IconMicrophone"; -import IconLink from "@tabler/icons-react-native/IconLink"; -import IconListNumbers from "@tabler/icons-react-native/IconListNumbers"; -import IconMenu2 from "@tabler/icons-react-native/IconMenu2"; -import IconMessageCircleQuestion from "@tabler/icons-react-native/IconMessageCircleQuestion"; -import IconMessage from "@tabler/icons-react-native/IconMessage"; -import IconMessagePlus from "@tabler/icons-react-native/IconMessagePlus"; -import IconMinus from "@tabler/icons-react-native/IconMinus"; -import IconMoon from "@tabler/icons-react-native/IconMoon"; -import IconNetwork from "@tabler/icons-react-native/IconNetwork"; -import IconPalette from "@tabler/icons-react-native/IconPalette"; -import IconPencil from "@tabler/icons-react-native/IconPencil"; -import IconPlug from "@tabler/icons-react-native/IconPlug"; -import IconPhoto from "@tabler/icons-react-native/IconPhoto"; -import IconPin from "@tabler/icons-react-native/IconPin"; -import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff"; -import IconPlayerPause from "@tabler/icons-react-native/IconPlayerPause"; -import IconPlayerPlay from "@tabler/icons-react-native/IconPlayerPlay"; -import IconPlayerStopFilled from "@tabler/icons-react-native/IconPlayerStopFilled"; -import IconPlus from "@tabler/icons-react-native/IconPlus"; -import IconQrcode from "@tabler/icons-react-native/IconQrcode"; -import IconRefresh from "@tabler/icons-react-native/IconRefresh"; -import IconSearch from "@tabler/icons-react-native/IconSearch"; -import IconShare from "@tabler/icons-react-native/IconShare"; -import IconServer from "@tabler/icons-react-native/IconServer"; -import IconSettings from "@tabler/icons-react-native/IconSettings"; -import IconSparkles from "@tabler/icons-react-native/IconSparkles"; -import IconStack2 from "@tabler/icons-react-native/IconStack2"; -import IconStar from "@tabler/icons-react-native/IconStar"; -import IconStarFilled from "@tabler/icons-react-native/IconStarFilled"; -import IconStethoscope from "@tabler/icons-react-native/IconStethoscope"; -import IconSun from "@tabler/icons-react-native/IconSun"; -import IconShieldQuestion from "@tabler/icons-react-native/IconShieldQuestion"; -import IconTarget from "@tabler/icons-react-native/IconTarget"; -import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2"; -import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease"; -import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease"; -import IconTool from "@tabler/icons-react-native/IconTool"; -import IconTrash from "@tabler/icons-react-native/IconTrash"; -import IconTypography from "@tabler/icons-react-native/IconTypography"; -import IconDownload from "@tabler/icons-react-native/IconDownload"; -import IconSitemap from "@tabler/icons-react-native/IconSitemap"; -import IconUpload from "@tabler/icons-react-native/IconUpload"; -import IconUserCircle from "@tabler/icons-react-native/IconUserCircle"; -import IconUserCheck from "@tabler/icons-react-native/IconUserCheck"; -import IconUserX from "@tabler/icons-react-native/IconUserX"; -import IconUserQuestion from "@tabler/icons-react-native/IconUserQuestion"; -import IconUserPlus from "@tabler/icons-react-native/IconUserPlus"; -import IconUsers from "@tabler/icons-react-native/IconUsers"; -import IconWifi from "@tabler/icons-react-native/IconWifi"; -import IconWifiOff from "@tabler/icons-react-native/IconWifiOff"; -import IconWorld from "@tabler/icons-react-native/IconWorld"; -import IconX from "@tabler/icons-react-native/IconX"; -import type { AndroidSymbol, SFSymbol, SymbolViewProps } from "expo-symbols"; +import IconFileDiff from "@tabler/icons-react-native/IconFileDiff"; +import { SymbolView as ExpoSymbolView } from "expo-symbols"; import { withUniwind } from "uniwind"; +import type { AppSymbolViewProps } from "./AppSymbol.android"; + +export type { SFSymbol } from "expo-symbols"; +export type { AppSymbolName, AppSymbolViewProps } from "./AppSymbol.android"; -const ANDROID_ICON_BY_SF_SYMBOL = { - plusminus: IconPlusMinus, +const CUSTOM_ICONS = { + "file.diff": IconFileDiff, + "arrow.triangle.pull": IconGitPullRequest, + "arrow.triangle.merge": IconGitMerge, + "arrow.triangle.branch": IconGitBranch, + "arrow.branch": IconGitBranch, "git.pull-request.closed": IconGitPullRequestClosed, "git.pull-request.draft": IconGitPullRequestDraft, "git.commit": IconGitCommit, - "arrow.branch": IconGitBranch, - "arrow.left": IconArrowLeft, - "arrow.right": IconArrowRight, - "arrow.left.arrow.right": IconArrowsLeftRight, - "arrow.clockwise": IconRefresh, - "arrow.down": IconArrowDown, - "arrow.down.circle": IconArrowDownCircle, - "arrow.right.circle": IconArrowRightCircle, - "arrow.triangle.branch": IconGitBranch, - "arrow.triangle.merge": IconGitMerge, - "arrow.triangle.pull": IconGitPullRequest, - "square.3.layers.3d": IconStack2, - "arrow.turn.left.up": IconArrowBackUp, - "arrow.up": IconArrowUp, - "arrow.up.circle": IconArrowUpCircle, - "arrow.up.left.and.arrow.down.right": IconArrowsDiagonal2, - "arrow.down.right.and.arrow.up.left": IconArrowsMinimize, - "arrow.up.right": IconArrowUpRight, - "arrow.up.right.circle": IconArrowUpRightCircle, - "arrow.uturn.backward": IconArrowBackUp, - "arrow.uturn.forward": IconArrowForwardUp, - archivebox: IconArchive, - "archivebox.fill": IconArchive, - "bell.badge": IconBellRinging, - "bolt.circle": IconBolt, - "bolt.horizontal.circle": IconBolt, - brain: IconBrain, - "cable.connector": IconPlug, - camera: IconCamera, - car: IconCar, - "chart.bar.xaxis": IconChartBar, - checkmark: IconCheck, - "checkmark.circle": IconCircleCheck, - "checkmark.circle.fill": IconCircleCheck, - circle: IconCircle, - "circle.fill": IconCircleFilled, - "circle.dashed": IconCircleDashed, - "circle.inset.filled": IconCircleDot, - "circle.dashed.inset.filled": IconCircleDot, - "calendar.badge.clock": IconCalendarClock, - clock: IconClock, - timer: IconClock, - "face.smiling": IconMoodPlus, - tag: IconTag, - ticket: IconTicket, - cloud: IconCloud, - cube: IconBox, - "chevron.down": IconChevronDown, - "chevron.left": IconChevronLeft, - "chevron.left.forwardslash.chevron.right": IconCode, - "chevron.right": IconChevronRight, - "chevron.up": IconChevronUp, - desktopcomputer: IconDeviceDesktop, - doc: IconFileText, - "doc.on.doc": IconCopy, - "doc.text": IconFileText, - "file.diff": IconFileDiff, - ellipsis: IconDots, - moon: IconMoon, - "ellipsis.circle": IconDotsCircleHorizontal, - "exclamationmark.triangle": IconAlertTriangle, - "exclamationmark.circle": IconAlertCircle, - eye: IconEye, - "eye.slash": IconEyeOff, - folder: IconFolder, - "folder.badge.plus": IconFolderPlus, - "folder.fill": IconFolder, - gearshape: IconSettings, - globe: IconWorld, - hammer: IconHammer, - headphones: IconHeadphones, - house: IconHome, - "info.circle": IconInfoCircle, - internaldrive: IconDatabase, - iphone: IconDeviceMobile, - keyboard: IconKeyboard, - laptopcomputer: IconDeviceLaptop, - link: IconLink, - "list.number": IconListNumbers, - "line.3.horizontal": IconMenu2, - "line.3.horizontal.decrease": IconFilter, - "line.3.horizontal.decrease.circle": IconFilter, - "line.3.horizontal.decrease.circle.fill": IconFilterFilled, - macmini: IconServer, - macstudio: IconDeviceDesktop, - magnifyingglass: IconSearch, - mic: IconMicrophone, - paintbrush: IconPalette, - pencil: IconPencil, - "person.crop.circle": IconUserCircle, - "person.2": IconUsers, - "person.fill.checkmark": IconUserCheck, - "person.fill.xmark": IconUserX, - "person.badge.clock": IconUserQuestion, - "person.badge.plus": IconUserPlus, - photo: IconPhoto, - pin: IconPin, - "pin.slash": IconPinnedOff, - play: IconPlayerPlay, - pause: IconPlayerPause, - plus: IconPlus, - minus: IconMinus, - "slash.circle": IconBan, - "qrcode.viewfinder": IconQrcode, - "point.3.connected.trianglepath.dotted": IconNetwork, - flowchart: IconSitemap, - "point.topleft.down.curvedto.point.bottomright.up": IconGitMerge, - safari: IconExternalLink, - "server.rack": IconServer, - stethoscope: IconStethoscope, - "sidebar.left": IconLayoutSidebar, - "sidebar.right": IconLayoutSidebarRight, - "slider.horizontal.3": IconAdjustmentsHorizontal, - "square.and.arrow.up": IconShare, - "square.and.pencil": IconEdit, - "square.on.square": IconCopy, - "square.grid.2x2": IconApps, - "square.split.2x1": IconLayoutColumns, - star: IconStar, - "star.fill": IconStarFilled, - "sun.max": IconSun, - "stop.fill": IconPlayerStopFilled, - target: IconTarget, - terminal: IconTerminal2, - "text.alignleft": IconAlignLeft, - "text.bubble": IconMessage, - "questionmark.bubble": IconMessageCircleQuestion, - "exclamationmark.shield": IconShieldQuestion, - "bubble.left.and.bubble.right": IconMessage, - "bubble.left": IconMessage, - sparkles: IconSparkles, - "plus.bubble": IconMessagePlus, - "text.word.spacing": IconLetterSpacing, - "textformat.size": IconTypography, - "textformat.size.larger": IconTextIncrease, - "textformat.size.smaller": IconTextDecrease, - "tray.and.arrow.down": IconDownload, - "tray.and.arrow.up": IconUpload, - trash: IconTrash, - wifi: IconWifi, - "wifi.slash": IconWifiOff, - xmark: IconX, - "xmark.circle.fill": IconCircleXFilled, -} satisfies Partial< - Record< - SFSymbol | "git.pull-request.closed" | "git.pull-request.draft" | "git.commit" | "file.diff", - Icon - > ->; -const SF_ICON_LOOKUP: Partial< - Record< - SFSymbol | "git.pull-request.closed" | "git.pull-request.draft" | "git.commit" | "file.diff", - Icon - > -> = ANDROID_ICON_BY_SF_SYMBOL; - -const ANDROID_ICON_BY_MATERIAL_NAME = { - auto_awesome: IconSparkles, - bolt: IconBolt, - build: IconTool, - chat: IconMessage, - chat_bubble: IconMessage, - check: IconCheck, - close: IconX, - construction: IconHammer, - content_copy: IconCopy, - desktop_windows: IconDeviceDesktop, - edit: IconEdit, - error: IconAlertCircle, - folder: IconFolder, - folder_open: IconFolderOpen, - keyboard: IconKeyboard, - keyboard_arrow_down: IconChevronDown, - keyboard_arrow_up: IconChevronUp, - keyboard_hide: IconKeyboardHide, - lock: IconLock, - more_vert: IconDotsVertical, - merge: IconGitMerge, - psychology: IconBrain, - public: IconWorld, - remove: IconMinus, - smartphone: IconDeviceMobile, - terminal: IconTerminal2, - visibility: IconEye, -} satisfies Partial>; - -export type { SFSymbol } from "expo-symbols"; -export type AppSymbolName = - | keyof typeof ANDROID_ICON_BY_SF_SYMBOL - | { - ios: SFSymbol; - android: keyof typeof ANDROID_ICON_BY_MATERIAL_NAME; - }; - -export type AppSymbolViewProps = Omit & { name: AppSymbolName }; - -export function isAppSymbolName(name: string): name is Extract { - return Object.prototype.hasOwnProperty.call(ANDROID_ICON_BY_SF_SYMBOL, name); -} +}; function AppSymbolView(props: AppSymbolViewProps) { - const materialName = typeof props.name === "string" ? undefined : props.name.android; - const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; - const AndroidIcon = - (materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ?? - (sfSymbol ? SF_ICON_LOOKUP[sfSymbol] : undefined); - - if (!AndroidIcon) { - return props.fallback ?? null; + const name = typeof props.name === "string" ? props.name : props.name.ios; + const CustomIcon = CUSTOM_ICONS[name as keyof typeof CUSTOM_ICONS]; + if (CustomIcon) { + return ( + + ); } - return ( - - ); + return ; } export const SymbolView = withUniwind(AppSymbolView); diff --git a/apps/mobile/src/components/FilePreviewGallery.tsx b/apps/mobile/src/components/ui/FilePreviewGallery.tsx similarity index 88% rename from apps/mobile/src/components/FilePreviewGallery.tsx rename to apps/mobile/src/components/ui/FilePreviewGallery.tsx index cd42342c7c0c4..0617520a581b8 100644 --- a/apps/mobile/src/components/FilePreviewGallery.tsx +++ b/apps/mobile/src/components/ui/FilePreviewGallery.tsx @@ -1,6 +1,6 @@ import { createContext, useContext } from "react"; import type { FilePreviewItem, FilePreviewSource } from "./FilePreviewModal.types"; -import { filePreviewGallery } from "../lib/filePreviewGallery"; +import { filePreviewGallery } from "../../lib/filePreviewGallery"; export const FilePreviewGallery = createContext([]); diff --git a/apps/mobile/src/components/FilePreview.tsx b/apps/mobile/src/components/ui/FilePreviewModal.android.tsx similarity index 59% rename from apps/mobile/src/components/FilePreview.tsx rename to apps/mobile/src/components/ui/FilePreviewModal.android.tsx index 26d2aa50248ab..b8fbf00c7fe08 100644 --- a/apps/mobile/src/components/FilePreview.tsx +++ b/apps/mobile/src/components/ui/FilePreviewModal.android.tsx @@ -1,20 +1,29 @@ -import { Button } from "./ui/Button"; -import * as Dialog from "./ui/Dialog"; -import { useEffect, useEffectEvent } from "react"; +import { Button } from "./Button"; +import * as Dialog from "./Dialog"; +import { createContext, useContext, useEffect, useEffectEvent } from "react"; import { Modal, View } from "react-native"; import ImageViewing from "react-native-image-viewing"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { openAttachmentInViewer } from "../lib/attachmentDownload"; -import { useMediaActions } from "../state/mediaActions"; +import { openAttachmentInViewer } from "../../lib/attachmentDownload"; +import { useMediaActions } from "../../state/mediaActions"; import type { + FilePreviewImages, + FilePreviewModalProps, + FilePreviewViewerProps, ResolvedFilePreviewDocument, +} from "./FilePreviewModal.types"; +import { FilePreviewSession } from "./FilePreviewSession"; +import { MediaPreviewHeader } from "../MediaPreviewHeader"; + +export type { + FilePreviewItem, + FilePreviewSource, ResolvedFilePreviewSource, } from "./FilePreviewModal.types"; -import { imageViewingProps, MediaImagePreview } from "./MediaImagePreview"; -import { PdfPreview } from "./PdfPreview"; -import { SheetHeader } from "./ui/SheetHeader"; -import { AppText as Text } from "./ui/AppText"; +import { PdfPreview } from "../PdfPreview"; +import { SheetHeader } from "./SheetHeader"; +import { AppText as Text } from "./AppText"; function PdfDocumentPreview(props: { readonly source: ResolvedFilePreviewDocument; @@ -113,16 +122,62 @@ function DocumentPreview(props: { ); } -export function FilePreview(props: { - readonly source: ResolvedFilePreviewSource; +type ImagePreviewProps = { + readonly source: FilePreviewImages; readonly onRequestClose: () => void; - readonly onOpenError?: (error: unknown) => void; -}) { +}; + +const ImagePreviewContext = createContext(null); + +function ImagePreviewHeader(header: { readonly imageIndex: number }) { + const props = useContext(ImagePreviewContext)!; + const source = props.source.images[header.imageIndex]; + const insets = useSafeAreaInsets(); + const mediaActions = useMediaActions(source?.actionsSource, props.onRequestClose); + return ( + + + + ); +} + +// Images still resolving show the viewer's loading page, and remount by key once their URL arrives. +function imageViewingProps(source: FilePreviewImages) { + return { + images: source.images.map((image) => (image.uri ? { uri: image.uri } : {})), + keyExtractor: (_: unknown, index: number) => source.images[index]?.uri ?? `pending:${index}`, + // Stays fixed: react-native-image-viewing remounts the Modal and replays its fade when it changes. + imageIndex: source.selectedIndex, + }; +} + +function ImagePreview(props: ImagePreviewProps) { + return ( + + + + ); +} + +function AndroidFilePreview(props: FilePreviewViewerProps) { const { source } = props; if (source.kind === "pdf") return ; if (source.kind !== "image") return ; if (source.images[source.selectedIndex]?.actionsSource) - return ; + return ; return ( ); } + +/** Shows images, PDFs and documents full screen. */ +export function FilePreviewModal(props: FilePreviewModalProps) { + return ; +} diff --git a/apps/mobile/src/components/FilePreview.ios.tsx b/apps/mobile/src/components/ui/FilePreviewModal.tsx similarity index 82% rename from apps/mobile/src/components/FilePreview.ios.tsx rename to apps/mobile/src/components/ui/FilePreviewModal.tsx index db62d2928fd4c..aff82eae479cd 100644 --- a/apps/mobile/src/components/FilePreview.ios.tsx +++ b/apps/mobile/src/components/ui/FilePreviewModal.tsx @@ -1,8 +1,19 @@ -import * as Dialog from "./ui/Dialog"; +import * as Dialog from "./Dialog"; import { requireNativeModule } from "expo"; import { useEffect, useEffectEvent, useId, useRef, useState } from "react"; -import type { FilePreviewImage, ResolvedFilePreviewSource } from "./FilePreviewModal.types"; +import type { + FilePreviewImage, + FilePreviewModalProps, + FilePreviewViewerProps, +} from "./FilePreviewModal.types"; +import { FilePreviewSession } from "./FilePreviewSession"; + +export type { + FilePreviewItem, + FilePreviewSource, + ResolvedFilePreviewSource, +} from "./FilePreviewModal.types"; const NativeControls = requireNativeModule<{ presentFile( @@ -25,11 +36,7 @@ const NativeControls = requireNativeModule<{ // Null tells the viewer an image failed; undefined means its URL has not arrived yet. const nativeUri = (image: FilePreviewImage) => (image.failed ? null : image.uri); -function NativeFilePreview(props: { - readonly source: ResolvedFilePreviewSource; - readonly onRequestClose: () => void; - readonly onOpenError?: (error: unknown) => void; -}) { +function NativeFilePreview(props: FilePreviewViewerProps) { const identifier = useId(); // Later URLs reach the open viewer through updateFile instead of presenting again. const [presented] = useState(props.source); @@ -91,10 +98,7 @@ function NativeFilePreview(props: { return null; } -export function FilePreview(props: { - readonly source: ResolvedFilePreviewSource; - readonly onRequestClose: () => void; - readonly onOpenError?: (error: unknown) => void; -}) { - return ; +/** Shows images, PDFs and documents full screen in the native viewer (Android: FilePreviewModal.android.tsx). */ +export function FilePreviewModal(props: FilePreviewModalProps) { + return ; } diff --git a/apps/mobile/src/components/FilePreviewModal.types.ts b/apps/mobile/src/components/ui/FilePreviewModal.types.ts similarity index 68% rename from apps/mobile/src/components/FilePreviewModal.types.ts rename to apps/mobile/src/components/ui/FilePreviewModal.types.ts index 83ee7207b8e47..37dd9ea9e16b0 100644 --- a/apps/mobile/src/components/FilePreviewModal.types.ts +++ b/apps/mobile/src/components/ui/FilePreviewModal.types.ts @@ -1,7 +1,7 @@ import type { AssetResource, EnvironmentId } from "@t3tools/contracts"; -import type { FileBackedComposerAttachment } from "../lib/composerImages"; -import type { MediaActionsSource } from "../lib/mediaActionsSource"; +import type { FileBackedComposerAttachment } from "../../lib/composerImages"; +import type { MediaActionsSource } from "../../lib/mediaActionsSource"; interface FilePreviewMetadata { readonly kind: "image" | "pdf" | "document"; @@ -42,3 +42,16 @@ export interface FilePreviewImages { } export type ResolvedFilePreviewSource = ResolvedFilePreviewDocument | FilePreviewImages; + +export interface FilePreviewModalProps { + readonly source: FilePreviewSource | null; + readonly onRequestClose: () => void; + readonly onOpenError?: (error: unknown) => void; +} + +/** A platform's viewer for one presentation, once FilePreviewSession has resolved it. */ +export interface FilePreviewViewerProps { + readonly source: ResolvedFilePreviewSource; + readonly onRequestClose: () => void; + readonly onOpenError?: (error: unknown) => void; +} diff --git a/apps/mobile/src/components/ui/FilePreviewSession.test.tsx b/apps/mobile/src/components/ui/FilePreviewSession.test.tsx new file mode 100644 index 0000000000000..07b75a58c1f76 --- /dev/null +++ b/apps/mobile/src/components/ui/FilePreviewSession.test.tsx @@ -0,0 +1,146 @@ +import { act, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vite-plus/test"; +import { EnvironmentId, SOURCE_CONTROL_MEDIA_ASSET } from "@t3tools/contracts"; + +const harness = vi.hoisted(() => ({ + refreshAssetUrl: vi.fn<(environmentId: string, resource: { url: string }) => Promise>(), + alert: vi.fn(), + previews: [] as unknown[], + mounts: 0, +})); + +vi.mock("../../state/assets", () => ({ refreshAssetUrl: harness.refreshAssetUrl })); +vi.mock("../../lib/localAttachmentPreview", () => ({ loadLocalAttachmentPreview: vi.fn() })); +vi.mock("../../lib/attachmentDownload", () => ({ + downloadAndShareAttachment: vi.fn(), + nativeViewerErrorMessage: () => "", + shareLocalAttachment: vi.fn(), +})); +vi.mock("../../lib/previewPresentation", () => ({ usePreviewPresentation: () => {} })); +vi.mock("./Dialog", () => ({ alert: harness.alert })); + +import type { FilePreviewItem, FilePreviewViewerProps } from "./FilePreviewModal.types"; +import { FilePreviewSession } from "./FilePreviewSession"; + +function Viewer(props: FilePreviewViewerProps) { + useState(() => harness.mounts++); + harness.previews.push(props.source); + return null; +} + +const environmentId = EnvironmentId.make("environment"); +const image = (url: string): FilePreviewItem => ({ + kind: "image", + environmentId, + resource: { _tag: SOURCE_CONTROL_MEDIA_ASSET, cwd: "/repo", url }, +}); + +async function openGallery( + selected: FilePreviewItem, + gallery: readonly FilePreviewItem[], + progressiveGallery: boolean, +) { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + harness.previews.length = 0; + harness.mounts = 0; + harness.alert.mockClear(); + const root = createRoot(document.createElement("div")); + const onRequestClose = vi.fn(); + await act(async () => + root.render( + , + ), + ); + return { onRequestClose, [Symbol.asyncDispose]: () => act(async () => root.unmount()) }; +} + +it.each([true, false])( + "keeps the gallery stable while a sibling is still resolving (progressive: %s)", + async (progressiveGallery) => { + const slow = Promise.withResolvers(); + harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => + resource.url.endsWith("slow.png") ? slow.promise : "https://signed.test/a.png", + ); + const selected = image("https://github.com/a.png"); + await using _preview = await openGallery( + selected, + [image("https://github.com/slow.png"), selected], + progressiveGallery, + ); + + if (progressiveGallery) { + expect(harness.previews.at(-1)).toMatchObject({ + kind: "image", + selectedIndex: 1, + images: [ + expect.not.objectContaining({ uri: expect.anything() }), + { uri: "https://signed.test/a.png" }, + ], + }); + } else { + expect(harness.previews).toEqual([]); + } + await act(async () => slow.resolve("https://signed.test/slow.png")); + expect(harness.previews.at(-1)).toMatchObject({ + selectedIndex: 1, + images: [{ uri: "https://signed.test/slow.png" }, { uri: "https://signed.test/a.png" }], + }); + expect(harness.mounts).toBe(1); + }, +); + +it.each([true, false])( + "handles a failed sibling without leaving a pending page (progressive: %s)", + async (progressiveGallery) => { + harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => { + if (resource.url.endsWith("missing.png")) throw new Error("not found"); + return `https://signed.test/${resource.url.split("/").pop()}`; + }); + const selected = image("https://github.com/a.png"); + await using preview = await openGallery( + selected, + [selected, image("https://github.com/missing.png"), image("https://github.com/c.png")], + progressiveGallery, + ); + + if (!progressiveGallery) { + expect(preview.onRequestClose).toHaveBeenCalledOnce(); + expect(harness.alert).toHaveBeenCalledOnce(); + expect(harness.previews).toEqual([]); + return; + } + + expect(preview.onRequestClose).not.toHaveBeenCalled(); + expect(harness.alert).not.toHaveBeenCalled(); + expect(harness.previews.at(-1)).toMatchObject({ + selectedIndex: 0, + images: [ + { uri: "https://signed.test/a.png" }, + { failed: true }, + { uri: "https://signed.test/c.png" }, + ], + }); + }, +); + +it("closes the preview when the selected image cannot be resolved", async () => { + harness.refreshAssetUrl.mockImplementation(async (_environmentId, resource) => { + if (resource.url.endsWith("missing.png")) throw new Error("not found"); + return `https://signed.test/${resource.url.split("/").pop()}`; + }); + const selected = image("https://github.com/missing.png"); + await using preview = await openGallery( + selected, + [image("https://github.com/a.png"), selected], + true, + ); + + expect(preview.onRequestClose).toHaveBeenCalledOnce(); + expect(harness.alert).toHaveBeenCalledOnce(); +}); diff --git a/apps/mobile/src/components/FilePreviewModal.tsx b/apps/mobile/src/components/ui/FilePreviewSession.tsx similarity index 82% rename from apps/mobile/src/components/FilePreviewModal.tsx rename to apps/mobile/src/components/ui/FilePreviewSession.tsx index 4d6c97464dd67..53c09b4d01392 100644 --- a/apps/mobile/src/components/FilePreviewModal.tsx +++ b/apps/mobile/src/components/ui/FilePreviewSession.tsx @@ -1,28 +1,31 @@ import * as Schema from "effect/Schema"; -import * as Dialog from "./ui/Dialog"; -import { useContext, useEffect, useEffectEvent, useState } from "react"; -import { Platform } from "react-native"; +import * as Dialog from "./Dialog"; +import { useContext, useEffect, useEffectEvent, useState, type ComponentType } from "react"; -import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview"; +import { loadLocalAttachmentPreview } from "../../lib/localAttachmentPreview"; import { downloadAndShareAttachment, nativeViewerErrorMessage, shareLocalAttachment, -} from "../lib/attachmentDownload"; -import { usePreviewPresentation } from "../lib/previewPresentation"; -import { refreshAssetUrl } from "../state/assets"; -import { filePreviewGallery, filePreviewKey, filePreviewName } from "../lib/filePreviewGallery"; +} from "../../lib/attachmentDownload"; +import { usePreviewPresentation } from "../../lib/previewPresentation"; +import { refreshAssetUrl } from "../../state/assets"; +import { filePreviewGallery, filePreviewKey, filePreviewName } from "../../lib/filePreviewGallery"; import { FilePreviewGallery } from "./FilePreviewGallery"; -import { FilePreview } from "./FilePreview"; -import type { MediaActionsSource } from "../lib/mediaActionsSource"; -import type { FilePreviewItem, FilePreviewSource } from "./FilePreviewModal.types"; - -export type { +import type { MediaActionsSource } from "../../lib/mediaActionsSource"; +import type { FilePreviewItem, + FilePreviewModalProps, FilePreviewSource, - ResolvedFilePreviewSource, + FilePreviewViewerProps, } from "./FilePreviewModal.types"; +interface FilePreviewSessionOptions { + /** Opens before every image resolves; the viewer takes later URLs as they arrive. */ + readonly progressiveGallery: boolean; + readonly Viewer: ComponentType; +} + class PreviewResolutionError extends Schema.TaggedError()( "PreviewResolutionError", { cause: Schema.Defect(), fallbackMessage: Schema.String }, @@ -80,16 +83,18 @@ function pdfActionsSource(item: FilePreviewItem): MediaActionsSource { return { ...metadata, environmentId: item.environmentId, resource: item.resource }; } -function ResolvedFilePreview(props: { - readonly source: FilePreviewSource; - readonly onRequestClose: () => void; - readonly onOpenError?: (error: unknown) => void; -}) { +function ResolvedFilePreview( + props: FilePreviewSessionOptions & { + readonly source: FilePreviewSource; + readonly onRequestClose: () => void; + readonly onOpenError?: (error: unknown) => void; + }, +) { + const { progressiveGallery, Viewer } = props; // Freeze the group at opening; streaming messages and URL refreshes must not reopen the viewer. const [items] = useState(() => filePreviewGallery(props.source, [])); const selectedKey = filePreviewKey(props.source); const selectedIndex = items.findIndex((item) => filePreviewKey(item) === selectedKey); - const progressiveGallery = Platform.OS === "ios"; // Undefined while resolving, null once it cannot be. const [uris, setUris] = useState(() => items.map((item) => ("uri" in item ? item.uri : undefined)), @@ -133,7 +138,7 @@ function ResolvedFilePreview(props: { }, [items, selectedIndex, progressiveGallery]); const selected = { ...items[selectedIndex]!, name: filePreviewName(items[selectedIndex]!) }; if (selected.kind === "image") { - // Android's viewer resets image gestures when its sources change during a presentation. + // A viewer that resets image gestures when its sources change waits for every image. if (!progressiveGallery && uris.some((uri) => uri === undefined)) return null; const images = items.map((item, index) => { const uri = uris[index]; @@ -144,7 +149,7 @@ function ResolvedFilePreview(props: { }; }); return ( - void; - - readonly onOpenError?: (error: unknown) => void; -}) { +/** + * The part of FilePreviewModal both platforms share: it resolves sources, freezes the gallery + * at opening and closes with its screen. Each platform's FilePreviewModal passes its viewer. + */ +export function FilePreviewSession(props: FilePreviewModalProps & FilePreviewSessionOptions) { const group = useContext(FilePreviewGallery); const source = props.source === null @@ -214,6 +218,8 @@ export function FilePreviewModal(props: { key={filePreviewKey(source)} source={source} onRequestClose={props.onRequestClose} + progressiveGallery={props.progressiveGallery} + Viewer={props.Viewer} {...(props.onOpenError ? { onOpenError: props.onOpenError } : {})} /> ); diff --git a/apps/mobile/src/components/ui/FrostedCutout.android.tsx b/apps/mobile/src/components/ui/FrostedCutout.android.tsx new file mode 100644 index 0000000000000..39e4b6c3900ef --- /dev/null +++ b/apps/mobile/src/components/ui/FrostedCutout.android.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { StyleSheet, View } from "react-native"; +import Svg, { Path } from "react-native-svg"; + +import type { FrostedCutoutProps } from "./FrostedCutout.types"; + +export function FrostedCutout(props: FrostedCutoutProps) { + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const x = (width - props.cutoutWidth) / 2; + const y = props.cutoutTop; + const w = props.cutoutWidth; + const h = props.cutoutHeight; + const r = Math.min(props.cutoutRadius, w / 2, h / 2); + const hole = `M${x + r},${y} H${x + w - r} A${r},${r} 0 0 1 ${x + w},${y + r} V${y + h - r} A${r},${r} 0 0 1 ${x + w - r},${y + h} H${x + r} A${r},${r} 0 0 1 ${x},${y + h - r} V${y + r} A${r},${r} 0 0 1 ${x + r},${y} Z`; + + return ( + { + setWidth(event.nativeEvent.layout.width); + setHeight(event.nativeEvent.layout.height); + }} + > + {width > 0 ? ( + + + + ) : null} + + ); +} diff --git a/apps/mobile/src/components/ui/FrostedCutout.ios.tsx b/apps/mobile/src/components/ui/FrostedCutout.ios.tsx deleted file mode 100644 index 66f098c6e22e2..0000000000000 --- a/apps/mobile/src/components/ui/FrostedCutout.ios.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { requireNativeView } from "expo"; -import { StyleSheet, type ViewProps } from "react-native"; - -import type { FrostedCutoutProps } from "./FrostedCutout.types"; - -const NativeFrostedCutout = requireNativeView( - "T3NativeControls", - "FrostedCutout", -); - -export function FrostedCutout(props: FrostedCutoutProps) { - return ; -} diff --git a/apps/mobile/src/components/ui/FrostedCutout.tsx b/apps/mobile/src/components/ui/FrostedCutout.tsx index 39e4b6c3900ef..66f098c6e22e2 100644 --- a/apps/mobile/src/components/ui/FrostedCutout.tsx +++ b/apps/mobile/src/components/ui/FrostedCutout.tsx @@ -1,38 +1,13 @@ -import { useState } from "react"; -import { StyleSheet, View } from "react-native"; -import Svg, { Path } from "react-native-svg"; +import { requireNativeView } from "expo"; +import { StyleSheet, type ViewProps } from "react-native"; import type { FrostedCutoutProps } from "./FrostedCutout.types"; -export function FrostedCutout(props: FrostedCutoutProps) { - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - const x = (width - props.cutoutWidth) / 2; - const y = props.cutoutTop; - const w = props.cutoutWidth; - const h = props.cutoutHeight; - const r = Math.min(props.cutoutRadius, w / 2, h / 2); - const hole = `M${x + r},${y} H${x + w - r} A${r},${r} 0 0 1 ${x + w},${y + r} V${y + h - r} A${r},${r} 0 0 1 ${x + w - r},${y + h} H${x + r} A${r},${r} 0 0 1 ${x},${y + h - r} V${y + r} A${r},${r} 0 0 1 ${x + r},${y} Z`; +const NativeFrostedCutout = requireNativeView( + "T3NativeControls", + "FrostedCutout", +); - return ( - { - setWidth(event.nativeEvent.layout.width); - setHeight(event.nativeEvent.layout.height); - }} - > - {width > 0 ? ( - - - - ) : null} - - ); +export function FrostedCutout(props: FrostedCutoutProps) { + return ; } diff --git a/apps/mobile/src/components/ui/Menu.android.tsx b/apps/mobile/src/components/ui/Menu.android.tsx index 025f9de93faab..f6153d9eccc91 100644 --- a/apps/mobile/src/components/ui/Menu.android.tsx +++ b/apps/mobile/src/components/ui/Menu.android.tsx @@ -1,6 +1,6 @@ import * as Haptics from "expo-haptics"; import { cloneElement, isValidElement, type ReactElement } from "react"; -import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu"; +import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu.android"; import type { MenuProps } from "./Menu.types"; export function Menu(props: MenuProps) { diff --git a/apps/mobile/src/components/ui/MenuPopup.android.tsx b/apps/mobile/src/components/ui/MenuPopup.android.tsx index dd6ef2b046566..06eeead1bd202 100644 --- a/apps/mobile/src/components/ui/MenuPopup.android.tsx +++ b/apps/mobile/src/components/ui/MenuPopup.android.tsx @@ -15,7 +15,7 @@ import { resolveScaledTextRole } from "../../lib/appearancePreferences"; import { useAndroidControlSizing } from "./useAndroidControlSizing"; import { useAppearancePreferences } from "../../features/settings/appearance/AppearancePreferencesProvider"; import type { MenuPopupProps } from "./MenuPopup"; -import { isAppSymbolName, SymbolView, type AppSymbolName } from "./AppSymbol"; +import { isAppSymbolName, SymbolView, type AppSymbolName } from "./AppSymbol.android"; // The Compose glyph host consumes touches; run the action here and keep accessibility on the row. function MenuIcon(props: { diff --git a/apps/mobile/src/components/ui/NativePresentation.android.tsx b/apps/mobile/src/components/ui/NativePresentation.android.tsx new file mode 100644 index 0000000000000..e342c6decc77a --- /dev/null +++ b/apps/mobile/src/components/ui/NativePresentation.android.tsx @@ -0,0 +1,7 @@ +import { View } from "react-native"; + +import type { PresentationSourceProps } from "./NativePresentation"; + +export function PresentationSource({ identifier: _identifier, ...props }: PresentationSourceProps) { + return ; +} diff --git a/apps/mobile/src/components/ui/NativePresentation.ios.tsx b/apps/mobile/src/components/ui/NativePresentation.ios.tsx deleted file mode 100644 index b93578dde602a..0000000000000 --- a/apps/mobile/src/components/ui/NativePresentation.ios.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { requireNativeView } from "expo"; -import type { ComponentType } from "react"; -import type { PresentationSourceProps } from "./NativePresentation"; - -const NativeSource: ComponentType = requireNativeView( - "T3NativeControls", - "PresentationSource", -); - -export function PresentationSource(props: PresentationSourceProps) { - return ; -} diff --git a/apps/mobile/src/components/ui/NativePresentation.tsx b/apps/mobile/src/components/ui/NativePresentation.tsx index 77510daf7ec28..61ff2b2b226af 100644 --- a/apps/mobile/src/components/ui/NativePresentation.tsx +++ b/apps/mobile/src/components/ui/NativePresentation.tsx @@ -1,11 +1,17 @@ -import type { ReactElement } from "react"; -import { View, type ViewProps } from "react-native"; +import { requireNativeView } from "expo"; +import type { ComponentType, ReactElement } from "react"; +import type { ViewProps } from "react-native"; export interface PresentationSourceProps extends ViewProps { readonly children: ReactElement; readonly identifier: string; } -export function PresentationSource({ identifier: _identifier, ...props }: PresentationSourceProps) { - return ; +const NativeSource: ComponentType = requireNativeView( + "T3NativeControls", + "PresentationSource", +); + +export function PresentationSource(props: PresentationSourceProps) { + return ; } diff --git a/apps/mobile/src/components/ui/ScreenHeaderButton.android.tsx b/apps/mobile/src/components/ui/ScreenHeaderButton.android.tsx index 905997a3da6e2..99bc9212d4071 100644 --- a/apps/mobile/src/components/ui/ScreenHeaderButton.android.tsx +++ b/apps/mobile/src/components/ui/ScreenHeaderButton.android.tsx @@ -1 +1 @@ -export { AndroidHeaderIconButton as ScreenHeaderButton } from "./AndroidScreenHeader"; +export { AndroidHeaderIconButton as ScreenHeaderButton } from "./AndroidScreenHeader.android"; diff --git a/apps/mobile/src/components/ui/SegmentedControl.android.tsx b/apps/mobile/src/components/ui/SegmentedControl.android.tsx new file mode 100644 index 0000000000000..bbcbc4a5f4593 --- /dev/null +++ b/apps/mobile/src/components/ui/SegmentedControl.android.tsx @@ -0,0 +1 @@ +export { SegmentedControl, type SegmentedControlProps } from "./SegmentedControl.shared"; diff --git a/apps/mobile/src/components/ui/SegmentedControl.ios.tsx b/apps/mobile/src/components/ui/SegmentedControl.ios.tsx deleted file mode 100644 index 15883d300da41..0000000000000 --- a/apps/mobile/src/components/ui/SegmentedControl.ios.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { requireNativeView } from "expo"; -import { View, type NativeSyntheticEvent, type ViewProps } from "react-native"; - -import { SegmentedControl as FlatSegmentedControl } from "./SegmentedControl.shared"; -import { segmentedControlColors } from "./segmentedControlColors"; -import { useControlTheme } from "./useControlTheme"; -import type { SegmentedControlProps } from "./SegmentedControl.types"; - -export type { SegmentedControlProps } from "./SegmentedControl.types"; - -const NativeSegmentedControl = requireNativeView< - ViewProps & { - labels: string[]; - selectedIndex: number; - trackColor: string; - selectedColor: string; - textColor: string; - mutedColor: string; - appearance: string; - onSelectionChange: (event: NativeSyntheticEvent<{ index: number }>) => void; - } ->("T3NativeControls", "SegmentedControl"); - -export function SegmentedControl( - props: SegmentedControlProps, -) { - const { colors, colorScheme, themeId } = useControlTheme(); - // UIKit adds a gray fill over the sunken card track. - if (props.variant === "card") return ; - const { trackColor, selectedColor, selectedTextColor, mutedColor } = segmentedControlColors( - colors, - colorScheme, - props.variant, - "ios", - themeId, - ); - return ( - - option.label)} - selectedIndex={props.options.findIndex((option) => option.value === props.selected)} - trackColor={trackColor} - selectedColor={selectedColor} - textColor={selectedTextColor} - mutedColor={mutedColor} - appearance={colorScheme} - onSelectionChange={({ nativeEvent }) => { - const option = props.options[nativeEvent.index]; - if (option) props.onSelect(option.value); - }} - style={{ height: props.size === "compact" ? 32 : 36 }} - /> - - ); -} diff --git a/apps/mobile/src/components/ui/SegmentedControl.tsx b/apps/mobile/src/components/ui/SegmentedControl.tsx index bbcbc4a5f4593..15883d300da41 100644 --- a/apps/mobile/src/components/ui/SegmentedControl.tsx +++ b/apps/mobile/src/components/ui/SegmentedControl.tsx @@ -1 +1,55 @@ -export { SegmentedControl, type SegmentedControlProps } from "./SegmentedControl.shared"; +import { requireNativeView } from "expo"; +import { View, type NativeSyntheticEvent, type ViewProps } from "react-native"; + +import { SegmentedControl as FlatSegmentedControl } from "./SegmentedControl.shared"; +import { segmentedControlColors } from "./segmentedControlColors"; +import { useControlTheme } from "./useControlTheme"; +import type { SegmentedControlProps } from "./SegmentedControl.types"; + +export type { SegmentedControlProps } from "./SegmentedControl.types"; + +const NativeSegmentedControl = requireNativeView< + ViewProps & { + labels: string[]; + selectedIndex: number; + trackColor: string; + selectedColor: string; + textColor: string; + mutedColor: string; + appearance: string; + onSelectionChange: (event: NativeSyntheticEvent<{ index: number }>) => void; + } +>("T3NativeControls", "SegmentedControl"); + +export function SegmentedControl( + props: SegmentedControlProps, +) { + const { colors, colorScheme, themeId } = useControlTheme(); + // UIKit adds a gray fill over the sunken card track. + if (props.variant === "card") return ; + const { trackColor, selectedColor, selectedTextColor, mutedColor } = segmentedControlColors( + colors, + colorScheme, + props.variant, + "ios", + themeId, + ); + return ( + + option.label)} + selectedIndex={props.options.findIndex((option) => option.value === props.selected)} + trackColor={trackColor} + selectedColor={selectedColor} + textColor={selectedTextColor} + mutedColor={mutedColor} + appearance={colorScheme} + onSelectionChange={({ nativeEvent }) => { + const option = props.options[nativeEvent.index]; + if (option) props.onSelect(option.value); + }} + style={{ height: props.size === "compact" ? 32 : 36 }} + /> + + ); +} diff --git a/apps/mobile/src/components/VideoPreviewModal.tsx b/apps/mobile/src/components/ui/VideoPreviewModal.android.tsx similarity index 87% rename from apps/mobile/src/components/VideoPreviewModal.tsx rename to apps/mobile/src/components/ui/VideoPreviewModal.android.tsx index 2f9d0063ca9eb..862d17d60d067 100644 --- a/apps/mobile/src/components/VideoPreviewModal.tsx +++ b/apps/mobile/src/components/ui/VideoPreviewModal.android.tsx @@ -2,25 +2,25 @@ import { videoMimeType } from "@t3tools/shared/video"; import { Modal, Pressable, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { AppActivityIndicator } from "./ui/AppActivityIndicator"; -import { useLocalAttachmentPreview } from "../lib/localAttachmentPreview"; -import { usePreviewPresentation, videoPreviewKey } from "../lib/previewPresentation"; -import { useMediaActions } from "../state/mediaActions"; -import { type MediaActionsSource } from "../lib/mediaActionsSource"; +import { AppActivityIndicator } from "./AppActivityIndicator"; +import { useLocalAttachmentPreview } from "../../lib/localAttachmentPreview"; +import { usePreviewPresentation, videoPreviewKey } from "../../lib/previewPresentation"; +import { useMediaActions } from "../../state/mediaActions"; +import { type MediaActionsSource } from "../../lib/mediaActionsSource"; import { mediaVideoPreviewUri, mediaVideoThumbnailKey, type LocalVideoPreviewSource, type MediaVideoPreviewSource, type VideoPreviewSource, -} from "../lib/videoPreviewSource"; -import { useAssetUrlState, useRefreshAssetUrl } from "../state/assets"; -import { usePreparedConnection } from "../state/session"; -import { AppText } from "./ui/AppText"; -import { MediaPreviewHeader } from "./MediaPreviewHeader"; -import { MediaVideoPlayer } from "./MediaVideoPlayer"; +} from "../../lib/videoPreviewSource"; +import { useAssetUrlState, useRefreshAssetUrl } from "../../state/assets"; +import { usePreparedConnection } from "../../state/session"; +import { AppText } from "./AppText"; +import { MediaPreviewHeader } from "../MediaPreviewHeader"; +import { MediaVideoPlayer } from "../MediaVideoPlayer"; -export type { VideoPreviewSource } from "../lib/videoPreviewSource"; +export type { VideoPreviewSource } from "../../lib/videoPreviewSource"; interface PlaybackState { readonly uri: string | null; diff --git a/apps/mobile/src/components/VideoPreviewModal.ios.tsx b/apps/mobile/src/components/ui/VideoPreviewModal.tsx similarity index 92% rename from apps/mobile/src/components/VideoPreviewModal.ios.tsx rename to apps/mobile/src/components/ui/VideoPreviewModal.tsx index 233cb063c42ed..89158669bbeed 100644 --- a/apps/mobile/src/components/VideoPreviewModal.ios.tsx +++ b/apps/mobile/src/components/ui/VideoPreviewModal.tsx @@ -1,23 +1,23 @@ -import * as Dialog from "./ui/Dialog"; +import * as Dialog from "./Dialog"; import { requireNativeModule } from "expo"; import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { AsyncResult } from "effect/reactivity"; import { useEffect, useEffectEvent, useId } from "react"; -import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview"; -import { usePreviewPresentation, videoPreviewKey } from "../lib/previewPresentation"; -import { fullscreenVideoClosed, fullscreenVideoOpened } from "../lib/videoAudioSession"; -import { mediaVideoPreviewUri, type VideoPreviewSource } from "../lib/videoPreviewSource"; -import { refreshAssetUrl } from "../state/assets"; -import { usePreparedConnection } from "../state/session"; +import { loadLocalAttachmentPreview } from "../../lib/localAttachmentPreview"; +import { usePreviewPresentation, videoPreviewKey } from "../../lib/previewPresentation"; +import { fullscreenVideoClosed, fullscreenVideoOpened } from "../../lib/videoAudioSession"; +import { mediaVideoPreviewUri, type VideoPreviewSource } from "../../lib/videoPreviewSource"; +import { refreshAssetUrl } from "../../state/assets"; +import { usePreparedConnection } from "../../state/session"; import { acquireAudioSession, activateAudioSession, isAudioSessionOwner, releaseAudioSession, -} from "../lib/audioSession"; +} from "../../lib/audioSession"; -export type { VideoPreviewSource } from "../lib/videoPreviewSource"; +export type { VideoPreviewSource } from "../../lib/videoPreviewSource"; const NativeControls = requireNativeModule<{ presentVideo( diff --git a/apps/mobile/src/components/ui/WorkspaceAppBar.android.tsx b/apps/mobile/src/components/ui/WorkspaceAppBar.android.tsx index 5a6fed51e5d98..0d98816ad2dfd 100644 --- a/apps/mobile/src/components/ui/WorkspaceAppBar.android.tsx +++ b/apps/mobile/src/components/ui/WorkspaceAppBar.android.tsx @@ -11,8 +11,8 @@ import { } from "react"; import { Pressable, View } from "react-native"; -import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu"; -import { AndroidHeaderIconButton } from "./AndroidScreenHeader"; +import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu.android"; +import { AndroidHeaderIconButton } from "./AndroidScreenHeader.android"; import { AppText } from "./AppText"; import { NativeAppBar } from "./NativeAppBar.android"; import { useControlTheme } from "./useControlTheme"; diff --git a/apps/mobile/src/features/files/AttachmentFileScreen.tsx b/apps/mobile/src/features/files/AttachmentFileScreen.tsx index d5e560e3267a9..fdf30e6f33b31 100644 --- a/apps/mobile/src/features/files/AttachmentFileScreen.tsx +++ b/apps/mobile/src/features/files/AttachmentFileScreen.tsx @@ -9,7 +9,7 @@ import { useRef, useState } from "react"; import { View } from "react-native"; import { AppBar, type AppBarMenu } from "../../components/ui/AppBar"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { useAttachmentDocument } from "../../lib/attachmentDocument"; import { nativeViewerErrorMessage } from "../../lib/attachmentDownload"; import { isFileBackedComposerAttachment } from "../../lib/composerImages"; diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index f9f81459bbf08..73629f02fd253 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -23,8 +23,8 @@ import { resolveMarkdownLinkPresentation, } from "../../lib/markdownLinks"; import { AppText as Text } from "../../components/ui/AppText"; -import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { VideoPreviewModal, type VideoPreviewSource } from "../../components/ui/VideoPreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { useMarkdownImageSource } from "../../native/useMarkdownImageSource"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index a5d3abe4c91d0..c51e05de37cd5 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -20,7 +20,7 @@ import { filePreviewDelimiter, parseDelimitedPreview } from "@t3tools/shared/del import { DelimitedFilePreview } from "./DelimitedFilePreview"; import { AudioFilePreview } from "./AudioFilePreview"; import { EmptyState } from "../../components/ui/EmptyState"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { LoadingScreen } from "../../components/LoadingScreen"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { isPdfFile } from "../../lib/filePreview"; diff --git a/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx b/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx index 60b2c902a76e5..55caeb0ee2e64 100644 --- a/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx +++ b/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx @@ -3,7 +3,7 @@ import { Image, Pressable, View } from "react-native"; import { FilePreviewLoading } from "./FilePreviewFeedback"; import { EmptyState } from "../../components/ui/EmptyState"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { PresentationSource } from "../../components/ui/NativePresentation"; import { useMediaActions } from "../../state/mediaActions"; import { type MediaActionsSource } from "../../lib/mediaActionsSource"; diff --git a/apps/mobile/src/features/files/ZipArchiveView.test.tsx b/apps/mobile/src/features/files/ZipArchiveView.test.tsx index 5b9dc2406973f..fac2d28f4ae2c 100644 --- a/apps/mobile/src/features/files/ZipArchiveView.test.tsx +++ b/apps/mobile/src/features/files/ZipArchiveView.test.tsx @@ -131,7 +131,7 @@ vi.mock("../../components/ui/useBarInset", () => ({ useContentTopInset: () => 0 vi.mock("../../components/ui/AppSymbol", () => ({ SymbolView: () => null })); vi.mock("../../components/ui/Progress", () => ({ Progress: () => null })); vi.mock("../../components/PierreEntryIcon", () => ({ PierreEntryIcon: () => null })); -vi.mock("../../components/FilePreviewModal", () => ({ FilePreviewModal: () => null })); +vi.mock("../../components/ui/FilePreviewModal", () => ({ FilePreviewModal: () => null })); vi.mock("../../lib/copyTextWithHaptic", () => ({})); vi.mock("./SourceFileSurface", () => ({ SourceFileSurface: (props: { readonly contents: string }) =>
{props.contents}
, diff --git a/apps/mobile/src/features/files/ZipArchiveView.tsx b/apps/mobile/src/features/files/ZipArchiveView.tsx index fb79723d6e39d..1d21ecf75e55a 100644 --- a/apps/mobile/src/features/files/ZipArchiveView.tsx +++ b/apps/mobile/src/features/files/ZipArchiveView.tsx @@ -29,7 +29,7 @@ import { EmptyState } from "../../components/ui/EmptyState"; import { Progress } from "../../components/ui/Progress"; import { NativeScrollView } from "../../native/NativeScrollViewMarker"; import * as Dialog from "../../components/ui/Dialog"; -import { FilePreviewModal } from "../../components/FilePreviewModal"; +import { FilePreviewModal } from "../../components/ui/FilePreviewModal"; import { nativeViewerErrorMessage, shareGeneratedAttachment, diff --git a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx index 59d2709a4f94a..2bee9303c9bf8 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx @@ -13,7 +13,7 @@ import { parseMarkdownDocument } from "../../lib/markdownDocument"; import { SymbolView } from "../../components/ui/AppSymbol"; import { AppText as Text } from "../../components/ui/AppText"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; -import type { FilePreviewItem } from "../../components/FilePreviewModal"; +import type { FilePreviewItem } from "../../components/ui/FilePreviewModal"; import { isBotBadgeImage } from "./botBadgeImages"; import { PullRequestMarkdown } from "./PullRequestMarkdown"; import { pullRequestDescriptionContent } from "./pullRequestDescriptionContent"; diff --git a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx index 41b93f966666b..05fe4b444a6ef 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx @@ -28,13 +28,13 @@ import { useOpenPullRequest } from "../../native/useOpenPullRequest"; import { SelectableMarkdownText } from "../../native/SelectableMarkdownText"; import { useMarkdownPreviewStyles } from "../files/FileMarkdownPreview"; import { markdownImageSources } from "../../lib/markdownImageSources"; -import { FilePreviewGallery, useFilePreviewGallery } from "../../components/FilePreviewGallery"; +import { FilePreviewGallery, useFilePreviewGallery } from "../../components/ui/FilePreviewGallery"; import { pullRequestImageSource } from "./pullRequestMediaSource"; import { FilePreviewModal, type FilePreviewSource, type FilePreviewItem, -} from "../../components/FilePreviewModal"; +} from "../../components/ui/FilePreviewModal"; import { ThreadMarkdownImageView } from "../threads/ThreadMarkdownImage"; import type { MarkdownImageRenderer } from "../../native/SelectableMarkdownText"; import type { SelectableMarkdownTextProps } from "../../native/SelectableMarkdownText.types"; diff --git a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx index 3fcfbceb842f4..fdd7b1abc4b67 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx @@ -4,7 +4,7 @@ import { pullRequestImageSource, type PullRequestMediaLocation, } from "./pullRequestMediaSource"; -import { useFilePreviewGallery } from "../../components/FilePreviewGallery"; +import { useFilePreviewGallery } from "../../components/ui/FilePreviewGallery"; import { videoMimeType } from "@t3tools/shared/video"; import { Pressable, View } from "react-native"; import { Image } from "expo-image"; @@ -13,13 +13,13 @@ import { useAssetUrlState, useRefreshAssetUrl } from "../../state/assets"; import { assetImageCacheKey } from "../../lib/assetImageSource"; import { MediaVideoPlayer } from "../../components/MediaVideoPlayer"; import { VideoAttachmentTile } from "../../components/VideoAttachmentTile"; -import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; +import { VideoPreviewModal, type VideoPreviewSource } from "../../components/ui/VideoPreviewModal"; import { ThreadMarkdownImageView } from "../threads/ThreadMarkdownImage"; import { FilePreviewModal, type FilePreviewSource, type FilePreviewItem, -} from "../../components/FilePreviewModal"; +} from "../../components/ui/FilePreviewModal"; export const PullRequestMediaVisibleContext = createContext(true); diff --git a/apps/mobile/src/features/pull-requests/pullRequestMediaSource.ts b/apps/mobile/src/features/pull-requests/pullRequestMediaSource.ts index d20bbeac6385c..dfcf8ad8a39a0 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestMediaSource.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestMediaSource.ts @@ -1,6 +1,6 @@ import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { SOURCE_CONTROL_MEDIA_ASSET, type EnvironmentId } from "@t3tools/contracts"; -import type { FilePreviewItem } from "../../components/FilePreviewModal.types"; +import type { FilePreviewItem } from "../../components/ui/FilePreviewModal.types"; export interface PullRequestMediaLocation { readonly environmentId: EnvironmentId; diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index e290f7ea498c5..d5fa085eb18fc 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from "react"; import { Platform, ScrollView, View, useWindowDimensions } from "react-native"; import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText"; import { SheetHeader } from "../../components/ui/SheetHeader"; diff --git a/apps/mobile/src/features/threads/QuestionAttachments.tsx b/apps/mobile/src/features/threads/QuestionAttachments.tsx index d98488c755dda..2c369c1e5b7bc 100644 --- a/apps/mobile/src/features/threads/QuestionAttachments.tsx +++ b/apps/mobile/src/features/threads/QuestionAttachments.tsx @@ -14,8 +14,8 @@ import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStri import { pickComposerFiles, pickComposerMedia } from "../../lib/composerImages"; import { useThreadSelection } from "../../state/use-thread-selection"; import { useNavigation } from "@react-navigation/native"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; -import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; +import { VideoPreviewModal, type VideoPreviewSource } from "../../components/ui/VideoPreviewModal"; import { useServerConfigs } from "../../state/entities"; import { appAtomRegistry } from "../../state/atom-registry"; import { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 8987d9fadc9f0..84fa6ae6d3185 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -35,7 +35,7 @@ import { useEffect, useEffectEvent, useRef, useState, type RefObject } from "rea import { I18nManager, Keyboard, Platform, View, type ViewStyle } from "react-native"; import { composerImageGallery } from "../../lib/composerImageGallery"; import { filePreviewGallery } from "../../lib/filePreviewGallery"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { composerAttachmentUploadBlockReason, composerAttachmentsStillUploading, @@ -76,7 +76,7 @@ import { ComposerAttachmentStrip, ComposerAttachmentThumbnail, } from "../../components/ComposerAttachmentStrip"; -import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; +import { VideoPreviewModal, type VideoPreviewSource } from "../../components/ui/VideoPreviewModal"; import { GlassSurface } from "../../components/ui/GlassSurface"; import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor"; import { fileRoutePathSegments } from "../files/filePath"; diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index d6fdc9e955cde..e4ae7f06d28d1 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -133,14 +133,14 @@ import { import { Image as ExpoImage } from "expo-image"; import { useResolveClassNames } from "uniwind"; import { useReduceMotionEnabled } from "../../lib/appState"; -import { useFilePreviewGallery, FilePreviewGallery } from "../../components/FilePreviewGallery"; +import { useFilePreviewGallery, FilePreviewGallery } from "../../components/ui/FilePreviewGallery"; import { assetImageSource } from "../../lib/assetImageSource"; import { composerImageGallery } from "../../lib/composerImageGallery"; import { isFileBackedComposerAttachment } from "../../lib/composerImages"; import { useLocalAttachmentPreview } from "../../lib/localAttachmentPreview"; import { messageImageGallery } from "./messageImageGallery"; import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; -import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; +import { FilePreviewModal, type FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { inlineFilePreviewKind, isPdfFile } from "../../lib/filePreview"; import { ThreadInlineFilePreview } from "./ThreadInlineFilePreview"; import { flattenThemeColor } from "../../lib/mobileTheme"; @@ -166,7 +166,7 @@ import { } from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/ui/AppText"; -import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; +import { VideoPreviewModal, type VideoPreviewSource } from "../../components/ui/VideoPreviewModal"; import { markdownFileEmbedLabel, resolveMarkdownMediaPreview } from "../../lib/markdownMedia"; import { attachmentVideoPreviewSource } from "../../lib/videoPreviewSource"; import { CopyTextButton } from "./CopyTextButton"; diff --git a/apps/mobile/src/features/threads/ThreadInlineFilePreview.tsx b/apps/mobile/src/features/threads/ThreadInlineFilePreview.tsx index e32c19a12786a..e44a422212712 100644 --- a/apps/mobile/src/features/threads/ThreadInlineFilePreview.tsx +++ b/apps/mobile/src/features/threads/ThreadInlineFilePreview.tsx @@ -3,7 +3,7 @@ import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments" import { useAudioPlayerStatus } from "expo-audio"; import { useContext, useEffect, useRef, useState } from "react"; import { AppState, Pressable, View } from "react-native"; -import type { FilePreviewSource } from "../../components/FilePreviewModal"; +import type { FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { filePreviewKey } from "../../lib/filePreviewGallery"; import { PierreEntryIcon } from "../../components/PierreEntryIcon"; import { AppText as Text } from "../../components/ui/AppText"; diff --git a/apps/mobile/src/features/threads/ThreadMarkdownImage.test.tsx b/apps/mobile/src/features/threads/ThreadMarkdownImage.test.tsx index 46da789a3fc2b..209cccf8d2ef0 100644 --- a/apps/mobile/src/features/threads/ThreadMarkdownImage.test.tsx +++ b/apps/mobile/src/features/threads/ThreadMarkdownImage.test.tsx @@ -30,7 +30,7 @@ vi.mock("expo/fetch", () => ({ fetch: vi.fn() })); vi.mock("react-native-svg", () => ({ parse: vi.fn(), SvgAst: () => null })); vi.mock("@t3tools/client-runtime/state/assets", () => ({ MISSING_FILE_ACCESS_MESSAGE: "" })); vi.mock("../../components/ui/AppActivityIndicator", () => ({ AppActivityIndicator: () => null })); -vi.mock("../../components/FilePreviewGallery", () => ({ +vi.mock("../../components/ui/FilePreviewGallery", () => ({ useFilePreviewGallery: () => (source: unknown) => source, })); vi.mock("../../components/ui/AppText", () => ({ diff --git a/apps/mobile/src/features/threads/ThreadMarkdownImage.tsx b/apps/mobile/src/features/threads/ThreadMarkdownImage.tsx index 81d569fafb466..da9ba935838c0 100644 --- a/apps/mobile/src/features/threads/ThreadMarkdownImage.tsx +++ b/apps/mobile/src/features/threads/ThreadMarkdownImage.tsx @@ -11,9 +11,9 @@ import { parse, SvgAst, type JsxAST } from "react-native-svg"; import { Pressable, StyleSheet, View, type ViewStyle } from "react-native"; import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator"; -import { useFilePreviewGallery } from "../../components/FilePreviewGallery"; +import { useFilePreviewGallery } from "../../components/ui/FilePreviewGallery"; import { AppText as Text } from "../../components/ui/AppText"; -import type { FilePreviewSource } from "../../components/FilePreviewModal"; +import type { FilePreviewSource } from "../../components/ui/FilePreviewModal"; import { MediaActionsMenu } from "../../components/MediaActionsMenu"; import { PresentationSource } from "../../components/ui/NativePresentation"; import { useMediaActions } from "../../state/mediaActions"; diff --git a/apps/mobile/src/features/threads/messageImageGallery.ts b/apps/mobile/src/features/threads/messageImageGallery.ts index c02269976a272..fb8fc3de67b78 100644 --- a/apps/mobile/src/features/threads/messageImageGallery.ts +++ b/apps/mobile/src/features/threads/messageImageGallery.ts @@ -1,6 +1,6 @@ import type { ChatAttachment, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { imageMimeType } from "@t3tools/shared/image"; -import type { FilePreviewItem } from "../../components/FilePreviewModal.types"; +import type { FilePreviewItem } from "../../components/ui/FilePreviewModal.types"; import { markdownImageSources } from "../../lib/markdownImageSources"; import { resolveMarkdownMediaPreview } from "../../lib/markdownMedia"; diff --git a/apps/mobile/src/features/threads/thread-work-log.tsx b/apps/mobile/src/features/threads/thread-work-log.tsx index 9851584d23b45..070ff4728e9aa 100644 --- a/apps/mobile/src/features/threads/thread-work-log.tsx +++ b/apps/mobile/src/features/threads/thread-work-log.tsx @@ -88,8 +88,8 @@ import { import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; import { notificationChildThreadId } from "@t3tools/client-runtime/state/thread-execution"; import type { MarkdownImageRenderer } from "../../native/SelectableMarkdownText"; -import type { FilePreviewSource } from "../../components/FilePreviewModal"; -import { FilePreviewGallery } from "../../components/FilePreviewGallery"; +import type { FilePreviewSource } from "../../components/ui/FilePreviewModal"; +import { FilePreviewGallery } from "../../components/ui/FilePreviewGallery"; import { ThreadMarkdownImage } from "./ThreadMarkdownImage"; import Animated, { FadeIn, diff --git a/apps/mobile/src/lib/composerImageGallery.ts b/apps/mobile/src/lib/composerImageGallery.ts index f308dac2ff19b..ffc6167e8b50d 100644 --- a/apps/mobile/src/lib/composerImageGallery.ts +++ b/apps/mobile/src/lib/composerImageGallery.ts @@ -1,6 +1,6 @@ import { imageMimeType } from "@t3tools/shared/image"; import type { EnvironmentId } from "@t3tools/contracts"; -import type { FilePreviewItem } from "../components/FilePreviewModal.types"; +import type { FilePreviewItem } from "../components/ui/FilePreviewModal.types"; import { isFileBackedComposerAttachment, type DraftComposerAttachment } from "./composerImages"; export function composerImageGallery( diff --git a/apps/mobile/src/lib/contextImageGallery.ts b/apps/mobile/src/lib/contextImageGallery.ts index 37803bd5923c6..335554b0565c5 100644 --- a/apps/mobile/src/lib/contextImageGallery.ts +++ b/apps/mobile/src/lib/contextImageGallery.ts @@ -1,6 +1,6 @@ import type { ComposerContextRecord, EnvironmentId } from "@t3tools/contracts"; import { imageMimeType } from "@t3tools/shared/image"; -import type { FilePreviewItem } from "../components/FilePreviewModal.types"; +import type { FilePreviewItem } from "../components/ui/FilePreviewModal.types"; import { composerImageGallery } from "./composerImageGallery"; import type { DraftComposerAttachment } from "./composerImages"; diff --git a/apps/mobile/src/lib/filePreview.test.ts b/apps/mobile/src/lib/filePreview.test.ts index 5dee41b54c167..d9b65cfbfc523 100644 --- a/apps/mobile/src/lib/filePreview.test.ts +++ b/apps/mobile/src/lib/filePreview.test.ts @@ -235,14 +235,14 @@ vi.mock("../components/ui/NativePresentation", () => ({ vi.mock("../components/ui/Menu", () => ({ Menu: ({ children }: { children?: ReactNode }) => children, })); -vi.mock("../components/FilePreviewGallery", () => ({ +vi.mock("../components/ui/FilePreviewGallery", () => ({ FilePreviewGallery: ({ children }: { children?: ReactNode }) => children, })); -vi.mock("../components/FilePreviewModal", () => ({ +vi.mock("../components/ui/FilePreviewModal", () => ({ FilePreviewModal: (props: { source: { name: string } | null }) => props.source ? createElement("div", { "data-file-preview": props.source.name }) : null, })); -vi.mock("../components/VideoPreviewModal", () => ({ VideoPreviewModal: () => null })); +vi.mock("../components/ui/VideoPreviewModal", () => ({ VideoPreviewModal: () => null })); vi.mock("../components/ComposerContextSheet", () => ({})); vi.mock("../native/useOpenPullRequest", () => ({ useOpenPullRequest: () => vi.fn() })); vi.mock("../native/useMarkdownImageSource", () => ({ useMarkdownImageSource: () => vi.fn() })); diff --git a/apps/mobile/src/lib/filePreviewGallery.test.ts b/apps/mobile/src/lib/filePreviewGallery.test.ts index 60576317b95c9..f85e33c851f07 100644 --- a/apps/mobile/src/lib/filePreviewGallery.test.ts +++ b/apps/mobile/src/lib/filePreviewGallery.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vite-plus/test"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import type { FilePreviewItem } from "../components/FilePreviewModal.types"; +import type { FilePreviewItem } from "../components/ui/FilePreviewModal.types"; import type { DraftComposerAttachment } from "./composerImages"; import { composerImageGallery } from "./composerImageGallery"; import { messageImageGallery } from "../features/threads/messageImageGallery"; diff --git a/apps/mobile/src/lib/filePreviewGallery.ts b/apps/mobile/src/lib/filePreviewGallery.ts index 4150054562442..295f46e861825 100644 --- a/apps/mobile/src/lib/filePreviewGallery.ts +++ b/apps/mobile/src/lib/filePreviewGallery.ts @@ -1,4 +1,4 @@ -import type { FilePreviewItem, FilePreviewSource } from "../components/FilePreviewModal.types"; +import type { FilePreviewItem, FilePreviewSource } from "../components/ui/FilePreviewModal.types"; export function filePreviewKey(source: FilePreviewItem): string { if ("resource" in source) diff --git a/apps/mobile/src/lib/markdownMedia.ts b/apps/mobile/src/lib/markdownMedia.ts index 31dcefb29f127..e03f91822eb23 100644 --- a/apps/mobile/src/lib/markdownMedia.ts +++ b/apps/mobile/src/lib/markdownMedia.ts @@ -17,7 +17,7 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { normalizeNativeMarkdownUrl, resolveMarkdownLinkPresentation } from "./markdownLinks"; import { resolveMobileMarkdownMediaSource } from "./markdownMediaSource"; -import type { FilePreviewSource } from "../components/FilePreviewModal"; +import type { FilePreviewSource } from "../components/ui/FilePreviewModal"; import type { MediaVideoPreviewSource } from "./videoPreviewSource"; import type { MediaActionsSource } from "./mediaActionsSource"; diff --git a/apps/mobile/src/lib/useMediaVideoPlayback.test.tsx b/apps/mobile/src/lib/useMediaVideoPlayback.test.tsx index 261017c49b06e..d5cc47250cb83 100644 --- a/apps/mobile/src/lib/useMediaVideoPlayback.test.tsx +++ b/apps/mobile/src/lib/useMediaVideoPlayback.test.tsx @@ -103,11 +103,11 @@ vi.mock("./videoThumbnails", () => ({ cachedVideoThumbnail: () => null, loadVideoThumbnail: mocks.thumbnail, })); -vi.mock("../components/FilePreviewGallery", () => ({ +vi.mock("../components/ui/FilePreviewGallery", () => ({ useFilePreviewGallery: () => (value: unknown) => value, })); -vi.mock("../components/FilePreviewModal", () => ({ FilePreviewModal: () => null })); -vi.mock("../components/VideoPreviewModal", () => ({ VideoPreviewModal: () => null })); +vi.mock("../components/ui/FilePreviewModal", () => ({ FilePreviewModal: () => null })); +vi.mock("../components/ui/VideoPreviewModal", () => ({ VideoPreviewModal: () => null })); vi.mock("../features/threads/ThreadMarkdownImage", () => ({ ThreadMarkdownImageView: () => null })); import { diff --git a/apps/mobile/src/native/MermaidCodeBlock.tsx b/apps/mobile/src/native/MermaidCodeBlock.tsx index beea0ee18b6d5..345f99b1b6355 100644 --- a/apps/mobile/src/native/MermaidCodeBlock.tsx +++ b/apps/mobile/src/native/MermaidCodeBlock.tsx @@ -6,7 +6,7 @@ import { Suspense, use, useRef, useState } from "react"; import { PixelRatio, Platform, Pressable, StyleSheet, Text, View } from "react-native"; import { parse, Svg, SvgXml } from "react-native-svg"; -import { MediaImagePreview } from "../components/MediaImagePreview"; +import { FilePreviewModal } from "../components/ui/FilePreviewModal"; import { SymbolView, type AppSymbolName } from "../components/ui/AppSymbol"; import { copyTextWithHaptic } from "../lib/copyTextWithHaptic"; import { mermaidSvgForNative, withDiagramBackground } from "../lib/mermaidSvg"; @@ -278,23 +278,20 @@ function MermaidBlock(props: MermaidCodeBlockProps & { readonly diagram: Diagram
) : null} {expansion?.status === "open" ? ( - setExpansion(null)} /> diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 5aab6e4a1cd3b..ef296d17cb572 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -27,7 +27,7 @@ const reactTests = [ "src/features/settings/components/BranchNamingSettings.test.tsx", "src/features/settings/components/SettingsDragHandle.test.tsx", "src/features/threads/ThreadMarkdownImage.test.tsx", - "src/components/FilePreviewModal.test.tsx", + "src/components/ui/FilePreviewSession.test.tsx", "src/features/threads/UsageLimitRecoveryCard.test.tsx", "src/features/threads/NewTaskDraftRouteScreen.test.tsx", "src/native/NativeHeaderToolbar.test.tsx", diff --git a/vite.config.ts b/vite.config.ts index fb6255cf96363..d891ec91f0545 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -396,27 +396,6 @@ export default defineConfig({ ], }, }, - { - files: ["apps/mobile/src/features/**"], - rules: { - "eslint/no-restricted-imports": [ - "error", - { - paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS], - patterns: [ - { - group: [ - "**/components/ui/AndroidScreenHeader", - "**/components/ui/WorkspaceAppBar*", - ], - importNames: ["AndroidScreenHeader", "WorkspaceAppBar"], - message: "Use AppBar, so every screen gets the same header and button metrics.", - }, - ], - }, - ], - }, - }, { files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"], rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] }, @@ -492,7 +471,7 @@ export default defineConfig({ files: ["apps/mobile/src/**"], excludeFiles: [ "apps/mobile/src/components/ui/AppSymbol.tsx", - "apps/mobile/src/components/ui/AppSymbol.ios.tsx", + "apps/mobile/src/components/ui/AppSymbol.android.tsx", "apps/mobile/src/features/pull-requests/pullRequestGlyphs.ts", ], rules: { "t3code/no-mobile-pull-request-glyph-literals": "error" },