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";...@@ -5,7 +5,7 @@ import type { MenuAction } from "@react-native-menu/menu";
5import { EnvironmentId } from "@t3tools/contracts";5import { EnvironmentId } from "@t3tools/contracts";
6import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";6import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
7import { useCallback, useEffect, useMemo, useRef, useState } from "react";7import { 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
10import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";10import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { AppText as Text } from "../../components/AppText";11import { AppText as Text } from "../../components/AppText";
...@@ -19,6 +19,7 @@ import { isFileBackedComposerAttachment } from "../../lib/composerImages";...@@ -19,6 +19,7 @@ import { isFileBackedComposerAttachment } from "../../lib/composerImages";
19import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";19import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
20import { useUniwindTheme } from "../../lib/useUniwindTheme";20import { useUniwindTheme } from "../../lib/useUniwindTheme";
21import { removeComposerDraftAttachment, useComposerDraft } from "../../state/use-composer-drafts";21import { removeComposerDraftAttachment, useComposerDraft } from "../../state/use-composer-drafts";
22import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback";
22import { FileMarkdownPreview } from "./FileMarkdownPreview";23import { FileMarkdownPreview } from "./FileMarkdownPreview";
23import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";24import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
24import { SourceFileSurface } from "./SourceFileSurface";25import { 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 file87 </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 @@
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...@@ -2,7 +2,7 @@ import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/Stac
2import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";2import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";
3import type { MenuAction } from "@react-native-menu/menu";3import type { MenuAction } from "@react-native-menu/menu";
4import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";4import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
5import { ActivityIndicator, Platform, View } from "react-native";5import { Platform, View } from "react-native";
6import { useSafeAreaInsets } from "react-native-safe-area-context";6import { useSafeAreaInsets } from "react-native-safe-area-context";
7import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";7import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
8import { EnvironmentId, type ProjectReadFileResult, ThreadId } from "@t3tools/contracts";8import { 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";
1616
17import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";17import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
18import { MaterialScreenContent } from "../../components/MaterialScreenContent";18import { MaterialScreenContent } from "../../components/MaterialScreenContent";
19import { AppText as Text } from "../../components/AppText";
20import { AudioFilePreview } from "../../components/AudioFilePreview";19import { AudioFilePreview } from "../../components/AudioFilePreview";
21import { ControlPillMenu } from "../../components/ControlPill";20import { ControlPillMenu } from "../../components/ControlPill";
22import { EmptyState } from "../../components/EmptyState";21import { 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";
50import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";49import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
51import { ThreadRouteScreen } from "../threads/ThreadRouteScreen";50import { ThreadRouteScreen } from "../threads/ThreadRouteScreen";
51import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback";
52import { FileMarkdownPreview } from "./FileMarkdownPreview";52import { FileMarkdownPreview } from "./FileMarkdownPreview";
53import { FileTreeBrowser } from "./FileTreeBrowser";53import { FileTreeBrowser } from "./FileTreeBrowser";
54import { useFileTreeEntries } from "./useFileTreeEntries";54import { useFileTreeEntries } from "./useFileTreeEntries";
...@@ -161,10 +161,7 @@ function FileContent(props: {...@@ -161,10 +161,7 @@ function FileContent(props: {
161161
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 }
197194
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 }
206198
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 file204 </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 <FileMarkdownPreview207 <FileMarkdownPreview
apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx+3-10
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { useId, useMemo, useState } from "react";1import { 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";
5import { EmptyState } from "../../components/EmptyState";5import { EmptyState } from "../../components/EmptyState";
6import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";6import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";
7import { PresentationSource } from "../../components/NativePresentation";7import { 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 }
8881
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";
3import { WebView } from "react-native-webview";3import { WebView } from "react-native-webview";
44
5import { AppText as Text } from "../../components/AppText";5import { AppText as Text } from "../../components/AppText";
6import { FilePreviewLoading } from "./FilePreviewFeedback";
6import { LoadingStrip } from "../../components/LoadingStrip";7import { LoadingStrip } from "../../components/LoadingStrip";
78
8export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) {9export 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);
1112
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 }
2016
21 return (17 return (