| author | |
| committer | |
| log | 58fcae64aece8198bcbbf051b02554ea893d859e |
| tree | 857e5a71a89fc24da6d6555f9d1e7db7f9fa82f0 |
| parent | 1a2a03f44b7ef6e3159a67a262c1586dbd3d3d5b |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 56 insertions(+), 62 deletions(-)
apps/mobile/src/features/files/AttachmentFileScreen.tsx+10-26| ... | @@ -5,7 +5,7 @@ import type { MenuAction } from "@react-native-menu/menu"; | ... | @@ -5,7 +5,7 @@ import type { MenuAction } from "@react-native-menu/menu"; |
| 5 | import { EnvironmentId } from "@t3tools/contracts"; | 5 | import { EnvironmentId } from "@t3tools/contracts"; |
| 6 | import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; | 6 | import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; |
| 7 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 7 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 8 | import { ActivityIndicator, Alert, Platform, ScrollView, View } from "react-native"; | 8 | import { Alert, Platform, ScrollView, View } from "react-native"; |
| 9 | 9 | ||
| 10 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 10 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 11 | import { AppText as Text } from "../../components/AppText"; | 11 | import { AppText as Text } from "../../components/AppText"; |
| ... | @@ -19,6 +19,7 @@ import { isFileBackedComposerAttachment } from "../../lib/composerImages"; | ... | @@ -19,6 +19,7 @@ import { isFileBackedComposerAttachment } from "../../lib/composerImages"; |
| 19 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; | 19 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 20 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 20 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 21 | import { removeComposerDraftAttachment, useComposerDraft } from "../../state/use-composer-drafts"; | 21 | import { removeComposerDraftAttachment, useComposerDraft } from "../../state/use-composer-drafts"; |
| 22 | import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback"; | ||
| 22 | import { FileMarkdownPreview } from "./FileMarkdownPreview"; | 23 | import { FileMarkdownPreview } from "./FileMarkdownPreview"; |
| 23 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 24 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 24 | import { SourceFileSurface } from "./SourceFileSurface"; | 25 | import { SourceFileSurface } from "./SourceFileSurface"; |
| ... | @@ -71,43 +72,26 @@ function AttachmentDocumentBody(props: { | ... | @@ -71,43 +72,26 @@ function AttachmentDocumentBody(props: { |
| 71 | ); | 72 | ); |
| 72 | } | 73 | } |
| 73 | if (props.nativeViewer !== null && props.nativeViewer !== "unavailable") { | 74 | if (props.nativeViewer !== null && props.nativeViewer !== "unavailable") { |
| 74 | return ( | 75 | return <FilePreviewLoading message="Opening in file viewer..." />; |
| 75 | <View className="flex-1 items-center justify-center gap-3 bg-sheet px-6"> | ||
| 76 | <ActivityIndicator /> | ||
| 77 | <Text className="text-center text-sm text-foreground-muted">Opening in file viewer...</Text> | ||
| 78 | </View> | ||
| 79 | ); | ||
| 80 | } | 76 | } |
| 81 | if (!document.uri || (document.needsText && !document.content)) { | 77 | if (!document.uri || (document.needsText && !document.content)) { |
| 82 | return ( | 78 | return <FilePreviewLoading message="Loading file..." />; |
| 83 | <View className="flex-1 items-center justify-center gap-3 bg-sheet px-6"> | ||
| 84 | <ActivityIndicator /> | ||
| 85 | <Text className="text-center text-sm text-foreground-muted">Loading file...</Text> | ||
| 86 | </View> | ||
| 87 | ); | ||
| 88 | } | 79 | } |
| 89 | if (document.needsText && document.content) { | 80 | if (document.needsText && document.content) { |
| 90 | const { content, table } = document; | 81 | const { content, table } = document; |
| 91 | return ( | 82 | return ( |
| 92 | <View className="flex-1 bg-sheet"> | 83 | <View className="flex-1 bg-sheet"> |
| 93 | {content.truncated ? ( | 84 | {content.truncated ? ( |
| 94 | <View className="border-b border-warning-border bg-warning px-4 py-2"> | 85 | <FilePreviewNotice title="Partial file"> |
| 95 | <Text className="text-2xs font-t3-bold uppercase text-warning-foreground"> | 86 | Preview limited to the first 1 MB. Save or share the file to read it in full. |
| 96 | Partial file | 87 | </FilePreviewNotice> |
| 97 | </Text> | ||
| 98 | <Text className="text-xs leading-snug text-warning-foreground"> | ||
| 99 | Preview limited to the first 1 MB. Save or share the file to read it in full. | ||
| 100 | </Text> | ||
| 101 | </View> | ||
| 102 | ) : null} | 88 | ) : null} |
| 103 | {table && document.activeMode === "table" ? ( | 89 | {table && document.activeMode === "table" ? ( |
| 104 | <ScrollView className="flex-1"> | 90 | <ScrollView className="flex-1"> |
| 105 | {table.truncated ? ( | 91 | {table.truncated ? ( |
| 106 | <View className="border-b border-warning-border bg-warning px-4 py-2"> | 92 | <FilePreviewNotice> |
| 107 | <Text className="text-xs leading-snug text-warning-foreground"> | 93 | Table limited to the first 100 rows and 30 columns. Source shows the rest. |
| 108 | Table limited to the first 100 rows and 30 columns. Source shows the rest. | 94 | </FilePreviewNotice> |
| 109 | </Text> | ||
| 110 | </View> | ||
| 111 | ) : null} | 95 | ) : null} |
| 112 | <ScrollView horizontal> | 96 | <ScrollView horizontal> |
| 113 | <View> | 97 | <View> |
apps/mobile/src/features/files/FilePreviewFeedback.tsx created+34| ... | @@ -0,0 +1,34 @@ | ||
| 1 | import { ActivityIndicator, View } from "react-native"; | ||
| 2 | |||
| 3 | import { AppText as Text } from "../../components/AppText"; | ||
| 4 | import { cn } from "../../lib/cn"; | ||
| 5 | |||
| 6 | export function FilePreviewLoading(props: { | ||
| 7 | readonly message: string; | ||
| 8 | readonly background?: "sheet" | "card"; | ||
| 9 | }) { | ||
| 10 | return ( | ||
| 11 | <View | ||
| 12 | className={cn( | ||
| 13 | "flex-1 items-center justify-center gap-3 px-6", | ||
| 14 | props.background === "card" ? "bg-card" : "bg-sheet", | ||
| 15 | )} | ||
| 16 | > | ||
| 17 | <ActivityIndicator /> | ||
| 18 | <Text className="text-center text-sm text-foreground-muted">{props.message}</Text> | ||
| 19 | </View> | ||
| 20 | ); | ||
| 21 | } | ||
| 22 | |||
| 23 | export function FilePreviewNotice(props: { readonly title?: string; readonly children: string }) { | ||
| 24 | return ( | ||
| 25 | <View className="border-b border-warning-border bg-warning px-4 py-2"> | ||
| 26 | {props.title ? ( | ||
| 27 | <Text className="text-2xs font-t3-bold uppercase text-warning-foreground"> | ||
| 28 | {props.title} | ||
| 29 | </Text> | ||
| 30 | ) : null} | ||
| 31 | <Text className="text-xs leading-snug text-warning-foreground">{props.children}</Text> | ||
| 32 | </View> | ||
| 33 | ); | ||
| 34 | } | ||
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+7-20| ... | @@ -2,7 +2,7 @@ import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/Stac | ... | @@ -2,7 +2,7 @@ import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/Stac |
| 2 | import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; | 2 | import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; |
| 3 | import type { MenuAction } from "@react-native-menu/menu"; | 3 | import type { MenuAction } from "@react-native-menu/menu"; |
| 4 | import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; | 4 | import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; |
| 5 | import { ActivityIndicator, Platform, View } from "react-native"; | 5 | import { Platform, View } from "react-native"; |
| 6 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 6 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 7 | import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; | 7 | import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; |
| 8 | import { EnvironmentId, type ProjectReadFileResult, ThreadId } from "@t3tools/contracts"; | 8 | import { EnvironmentId, type ProjectReadFileResult, ThreadId } from "@t3tools/contracts"; |
| ... | @@ -16,7 +16,6 @@ import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; | ... | @@ -16,7 +16,6 @@ import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; |
| 16 | 16 | ||
| 17 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 17 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 18 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; | 18 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| 19 | import { AppText as Text } from "../../components/AppText"; | ||
| 20 | import { AudioFilePreview } from "../../components/AudioFilePreview"; | 19 | import { AudioFilePreview } from "../../components/AudioFilePreview"; |
| 21 | import { ControlPillMenu } from "../../components/ControlPill"; | 20 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 22 | import { EmptyState } from "../../components/EmptyState"; | 21 | import { EmptyState } from "../../components/EmptyState"; |
| ... | @@ -49,6 +48,7 @@ import { | ... | @@ -49,6 +48,7 @@ import { |
| 49 | } from "../layout/workspace-sidebar-toolbar"; | 48 | } from "../layout/workspace-sidebar-toolbar"; |
| 50 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 49 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 51 | import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; | 50 | import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; |
| 51 | import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback"; | ||
| 52 | import { FileMarkdownPreview } from "./FileMarkdownPreview"; | 52 | import { FileMarkdownPreview } from "./FileMarkdownPreview"; |
| 53 | import { FileTreeBrowser } from "./FileTreeBrowser"; | 53 | import { FileTreeBrowser } from "./FileTreeBrowser"; |
| 54 | import { useFileTreeEntries } from "./useFileTreeEntries"; | 54 | import { useFileTreeEntries } from "./useFileTreeEntries"; |
| ... | @@ -161,10 +161,7 @@ function FileContent(props: { | ... | @@ -161,10 +161,7 @@ function FileContent(props: { |
| 161 | 161 | ||
| 162 | if (isAudioFile) { | 162 | if (isAudioFile) { |
| 163 | return props.previewUri === null ? ( | 163 | return props.previewUri === null ? ( |
| 164 | <View className="flex-1 items-center justify-center gap-3 bg-sheet px-6"> | 164 | <FilePreviewLoading message="Loading file..." /> |
| 165 | <ActivityIndicator /> | ||
| 166 | <Text className="text-center text-sm text-foreground-muted">Loading file...</Text> | ||
| 167 | </View> | ||
| 168 | ) : ( | 165 | ) : ( |
| 169 | <AudioFilePreview uri={props.previewUri} onRetry={props.onRetryPreview} /> | 166 | <AudioFilePreview uri={props.previewUri} onRetry={props.onRetryPreview} /> |
| 170 | ); | 167 | ); |
| ... | @@ -196,25 +193,15 @@ function FileContent(props: { | ... | @@ -196,25 +193,15 @@ function FileContent(props: { |
| 196 | } | 193 | } |
| 197 | 194 | ||
| 198 | if (props.fileContents === null) { | 195 | if (props.fileContents === null) { |
| 199 | return ( | 196 | return <FilePreviewLoading message="Loading file..." />; |
| 200 | <View className="flex-1 items-center justify-center gap-3 bg-sheet px-6"> | ||
| 201 | <ActivityIndicator /> | ||
| 202 | <Text className="text-center text-sm text-foreground-muted">Loading file...</Text> | ||
| 203 | </View> | ||
| 204 | ); | ||
| 205 | } | 197 | } |
| 206 | 198 | ||
| 207 | return ( | 199 | return ( |
| 208 | <View className="flex-1 bg-sheet"> | 200 | <View className="flex-1 bg-sheet"> |
| 209 | {props.truncated ? ( | 201 | {props.truncated ? ( |
| 210 | <View className="border-b border-warning-border bg-warning px-4 py-2"> | 202 | <FilePreviewNotice title="Partial file"> |
| 211 | <Text className="text-2xs font-t3-bold uppercase text-warning-foreground"> | 203 | Preview limited to the first 1 MB of a truncated file. |
| 212 | Partial file | 204 | </FilePreviewNotice> |
| 213 | </Text> | ||
| 214 | <Text className="text-xs leading-snug text-warning-foreground"> | ||
| 215 | Preview limited to the first 1 MB of a truncated file. | ||
| 216 | </Text> | ||
| 217 | </View> | ||
| 218 | ) : null} | 205 | ) : null} |
| 219 | {props.activeMode === "preview" && isMarkdown ? ( | 206 | {props.activeMode === "preview" && isMarkdown ? ( |
| 220 | <FileMarkdownPreview | 207 | <FileMarkdownPreview |
apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx+3-10| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { useId, useMemo, useState } from "react"; | 1 | import { useId, useMemo, useState } from "react"; |
| 2 | import { ActivityIndicator, Image, Pressable, View } from "react-native"; | 2 | import { Image, Pressable, View } from "react-native"; |
| 3 | 3 | ||
| 4 | import { AppText as Text } from "../../components/AppText"; | 4 | import { FilePreviewLoading } from "./FilePreviewFeedback"; |
| 5 | import { EmptyState } from "../../components/EmptyState"; | 5 | import { EmptyState } from "../../components/EmptyState"; |
| 6 | import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; | 6 | import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; |
| 7 | import { PresentationSource } from "../../components/NativePresentation"; | 7 | import { PresentationSource } from "../../components/NativePresentation"; |
| ... | @@ -76,14 +76,7 @@ export function WorkspaceFileImagePreview(props: { | ... | @@ -76,14 +76,7 @@ export function WorkspaceFileImagePreview(props: { |
| 76 | readonly actionsSource?: MediaActionsSource; | 76 | readonly actionsSource?: MediaActionsSource; |
| 77 | }) { | 77 | }) { |
| 78 | if (props.uri === null) { | 78 | if (props.uri === null) { |
| 79 | return ( | 79 | return <FilePreviewLoading message="Preparing image preview..." background="card" />; |
| 80 | <View className="flex-1 items-center justify-center gap-3 bg-card px-6"> | ||
| 81 | <ActivityIndicator /> | ||
| 82 | <Text className="text-center text-sm text-foreground-muted"> | ||
| 83 | Preparing image preview... | ||
| 84 | </Text> | ||
| 85 | </View> | ||
| 86 | ); | ||
| 87 | } | 80 | } |
| 88 | 81 | ||
| 89 | return ( | 82 | return ( |
apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx+2-6| ... | @@ -3,6 +3,7 @@ import { ActivityIndicator, View } from "react-native"; | ... | @@ -3,6 +3,7 @@ import { ActivityIndicator, View } from "react-native"; |
| 3 | import { WebView } from "react-native-webview"; | 3 | import { WebView } from "react-native-webview"; |
| 4 | 4 | ||
| 5 | import { AppText as Text } from "../../components/AppText"; | 5 | import { AppText as Text } from "../../components/AppText"; |
| 6 | import { FilePreviewLoading } from "./FilePreviewFeedback"; | ||
| 6 | import { LoadingStrip } from "../../components/LoadingStrip"; | 7 | import { LoadingStrip } from "../../components/LoadingStrip"; |
| 7 | 8 | ||
| 8 | export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) { | 9 | export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) { |
| ... | @@ -10,12 +11,7 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) | ... | @@ -10,12 +11,7 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) |
| 10 | const [loadError, setLoadError] = useState<string | null>(null); | 11 | const [loadError, setLoadError] = useState<string | null>(null); |
| 11 | 12 | ||
| 12 | if (props.uri === null) { | 13 | if (props.uri === null) { |
| 13 | return ( | 14 | return <FilePreviewLoading message="Preparing preview..." background="card" />; |
| 14 | <View className="flex-1 items-center justify-center gap-3 bg-card px-6"> | ||
| 15 | <ActivityIndicator /> | ||
| 16 | <Text className="text-center text-sm text-foreground-muted">Preparing preview...</Text> | ||
| 17 | </View> | ||
| 18 | ); | ||
| 19 | } | 15 | } |
| 20 | 16 | ||
| 21 | return ( | 17 | return ( |