authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-17 20:20:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 20:20:21-07:00
log58fcae64aece8198bcbbf051b02554ea893d859e
tree857e5a71a89fc24da6d6555f9d1e7db7f9fa82f0
parent1a2a03f44b7ef6e3159a67a262c1586dbd3d3d5b
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): share file preview feedback (#12368)


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";
55import { EnvironmentId } from "@t3tools/contracts";
66import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
77import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8import { ActivityIndicator, Alert, Platform, ScrollView, View } from "react-native";
8import { Alert, Platform, ScrollView, View } from "react-native";
99
1010import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
1111import { AppText as Text } from "../../components/AppText";
......@@ -19,6 +19,7 @@ import { isFileBackedComposerAttachment } from "../../lib/composerImages";
1919import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
2020import { useUniwindTheme } from "../../lib/useUniwindTheme";
2121import { removeComposerDraftAttachment, useComposerDraft } from "../../state/use-composer-drafts";
22import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback";
2223import { FileMarkdownPreview } from "./FileMarkdownPreview";
2324import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
2425import { SourceFileSurface } from "./SourceFileSurface";
......@@ -71,43 +72,26 @@ function AttachmentDocumentBody(props: {
7172 );
7273 }
7374 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..." />;
8076 }
8177 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..." />;
8879 }
8980 if (document.needsText && document.content) {
9081 const { content, table } = document;
9182 return (
9283 <View className="flex-1 bg-sheet">
9384 {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>
10288 ) : null}
10389 {table && document.activeMode === "table" ? (
10490 <ScrollView className="flex-1">
10591 {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>
11195 ) : null}
11296 <ScrollView horizontal>
11397 <View>
apps/mobile/src/features/files/FilePreviewFeedback.tsx created+34
......@@ -0,0 +1,34 @@
1import { ActivityIndicator, View } from "react-native";
2
3import { AppText as Text } from "../../components/AppText";
4import { cn } from "../../lib/cn";
5
6export 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
23export 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
22import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";
33import type { MenuAction } from "@react-native-menu/menu";
44import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
5import { ActivityIndicator, Platform, View } from "react-native";
5import { Platform, View } from "react-native";
66import { useSafeAreaInsets } from "react-native-safe-area-context";
77import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
88import { EnvironmentId, type ProjectReadFileResult, ThreadId } from "@t3tools/contracts";
......@@ -16,7 +16,6 @@ import { mediaFileReference } from "@t3tools/client-runtime/media-reference";
1616
1717import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
1818import { MaterialScreenContent } from "../../components/MaterialScreenContent";
19import { AppText as Text } from "../../components/AppText";
2019import { AudioFilePreview } from "../../components/AudioFilePreview";
2120import { ControlPillMenu } from "../../components/ControlPill";
2221import { EmptyState } from "../../components/EmptyState";
......@@ -49,6 +48,7 @@ import {
4948} from "../layout/workspace-sidebar-toolbar";
5049import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
5150import { ThreadRouteScreen } from "../threads/ThreadRouteScreen";
51import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback";
5252import { FileMarkdownPreview } from "./FileMarkdownPreview";
5353import { FileTreeBrowser } from "./FileTreeBrowser";
5454import { useFileTreeEntries } from "./useFileTreeEntries";
......@@ -161,10 +161,7 @@ function FileContent(props: {
161161
162162 if (isAudioFile) {
163163 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..." />
168165 ) : (
169166 <AudioFilePreview uri={props.previewUri} onRetry={props.onRetryPreview} />
170167 );
......@@ -196,25 +193,15 @@ function FileContent(props: {
196193 }
197194
198195 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..." />;
205197 }
206198
207199 return (
208200 <View className="flex-1 bg-sheet">
209201 {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>
218205 ) : null}
219206 {props.activeMode === "preview" && isMarkdown ? (
220207 <FileMarkdownPreview
apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx+3-10
......@@ -1,7 +1,7 @@
11import { useId, useMemo, useState } from "react";
2import { ActivityIndicator, Image, Pressable, View } from "react-native";
2import { Image, Pressable, View } from "react-native";
33
4import { AppText as Text } from "../../components/AppText";
4import { FilePreviewLoading } from "./FilePreviewFeedback";
55import { EmptyState } from "../../components/EmptyState";
66import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";
77import { PresentationSource } from "../../components/NativePresentation";
......@@ -76,14 +76,7 @@ export function WorkspaceFileImagePreview(props: {
7676 readonly actionsSource?: MediaActionsSource;
7777}) {
7878 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" />;
8780 }
8881
8982 return (
apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx+2-6
......@@ -3,6 +3,7 @@ import { ActivityIndicator, View } from "react-native";
33import { WebView } from "react-native-webview";
44
55import { AppText as Text } from "../../components/AppText";
6import { FilePreviewLoading } from "./FilePreviewFeedback";
67import { LoadingStrip } from "../../components/LoadingStrip";
78
89export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) {
......@@ -10,12 +11,7 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null })
1011 const [loadError, setLoadError] = useState<string | null>(null);
1112
1213 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" />;
1915 }
2016
2117 return (