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