Skip to content
41 changes: 41 additions & 0 deletions apps/mobile/src/components/FullScreenImageViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useCallback, useMemo } from "react";
import ImageViewing from "react-native-image-viewing";

import type { FullScreenImageSource } from "../lib/fullScreenImageActions";
import { useShareImage } from "../lib/useShareImage";

/**
* Fullscreen image viewer. Long-pressing the image opens the system share
* sheet, which is where both platforms already put Copy and Save Image, so
* this owns no chrome of its own.
*/
export function FullScreenImageViewer(props: {
readonly source: FullScreenImageSource | null;
readonly onRequestClose: () => void;
}) {
const { source } = props;
const share = useShareImage();

const images = useMemo(
() => (source === null ? [] : [{ uri: source.uri, cache: source.cache }]),
[source],
);

const onLongPress = useCallback(() => {
if (source !== null) {
share(source);
}
}, [share, source]);

return (
<ImageViewing
images={images}
imageIndex={0}
visible={source !== null}
onRequestClose={props.onRequestClose}
onLongPress={onLongPress}
swipeToCloseEnabled
doubleTapToZoomEnabled
/>
);
}
24 changes: 13 additions & 11 deletions apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { useAtomValue } from "@effect/atom-react";
import { useMemo, useState } from "react";
import { ActivityIndicator, Image, Pressable, View } from "react-native";
import ImageViewing from "react-native-image-viewing";
import { AsyncResult } from "effect/unstable/reactivity";

import { AppText as Text } from "../../components/AppText";
import { EmptyState } from "../../components/EmptyState";
import { FullScreenImageViewer } from "../../components/FullScreenImageViewer";
import type { FullScreenImageSource } from "../../lib/fullScreenImageActions";
import { workspaceFileImageAtom } from "./workspace-file-image-cache";

function ResolvedWorkspaceFileImagePreview(props: {
readonly accessibilityLabel: string;
readonly uri: string;
}) {
const [loadError, setLoadError] = useState<string | null>(null);
const [fullScreenVisible, setFullScreenVisible] = useState(false);
const [fullScreenSource, setFullScreenSource] = useState<FullScreenImageSource | null>(null);
const imageSource = useMemo(
() => ({ uri: props.uri, cache: "force-cache" as const }),
[props.uri],
);
const fullScreenImages = useMemo(() => [imageSource], [imageSource]);

return (
<View className="relative flex-1 bg-subtle">
Expand All @@ -27,7 +27,13 @@ function ResolvedWorkspaceFileImagePreview(props: {
accessibilityLabel={`Open full-screen preview of ${props.accessibilityLabel}`}
disabled={loadError !== null}
className="flex-1 p-4 active:bg-subtle-strong"
onPress={() => setFullScreenVisible(true)}
onPress={() =>
setFullScreenSource({
uri: props.uri,
fileName: props.accessibilityLabel,
cache: "force-cache",
})
}
>
<Image
accessible={false}
Expand All @@ -47,13 +53,9 @@ function ResolvedWorkspaceFileImagePreview(props: {
</View>
) : null}

<ImageViewing
images={fullScreenImages}
imageIndex={0}
visible={fullScreenVisible}
onRequestClose={() => setFullScreenVisible(false)}
swipeToCloseEnabled
doubleTapToZoomEnabled
<FullScreenImageViewer
source={fullScreenSource}
onRequestClose={() => setFullScreenSource(null)}
/>
</View>
);
Expand Down
14 changes: 7 additions & 7 deletions apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
} from "react-native";
import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ImageViewing from "react-native-image-viewing";

import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
import { SymbolView } from "../../components/AppSymbol";
import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip";
import { ControlPill } from "../../components/ControlPill";
import { FullScreenImageViewer } from "../../components/FullScreenImageViewer";
import { cn } from "../../lib/cn";
import type { DraftComposerImageAttachment } from "../../lib/composerImages";
import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";
Expand Down Expand Up @@ -63,6 +63,10 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
>({});
const [attachments, setAttachments] = useState<ReadonlyArray<DraftComposerImageAttachment>>([]);
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
const previewSource = useMemo(
() => (previewImageUri === null ? null : { uri: previewImageUri }),
[previewImageUri],
);

const selectedLines = useMemo(
() => (target ? getSelectedReviewCommentLines(target) : []),
Expand Down Expand Up @@ -337,13 +341,9 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
</View>
</KeyboardStickyView>
) : null}
<ImageViewing
images={previewImageUri ? [{ uri: previewImageUri }] : []}
imageIndex={0}
visible={previewImageUri !== null}
<FullScreenImageViewer
source={previewSource}
onRequestClose={() => setPreviewImageUri(null)}
swipeToCloseEnabled
doubleTapToZoomEnabled
/>
</View>
);
Expand Down
15 changes: 6 additions & 9 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
View,
type ViewStyle,
} from "react-native";
import ImageViewing from "react-native-image-viewing";
import Animated, {
FadeIn,
FadeInDown,
Expand All @@ -51,6 +50,7 @@ import {
ComposerToolbarTrigger,
} from "../../components/ComposerToolbarTrigger";
import { ControlPill, ControlPillMenu } from "../../components/ControlPill";
import { FullScreenImageViewer } from "../../components/FullScreenImageViewer";
import { ProviderIcon } from "../../components/ProviderIcon";
import type { DraftComposerImageAttachment } from "../../lib/composerImages";
import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
Expand Down Expand Up @@ -277,6 +277,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
const { onExpandedChange } = props;

const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
const previewSource = useMemo(
() => (previewImageUri === null ? null : { uri: previewImageUri }),
[previewImageUri],
);
const hasContent = props.draftMessage.trim().length > 0 || props.draftAttachments.length > 0;
const isExpanded = isFocused;
const canSend = hasContent;
Expand Down Expand Up @@ -922,14 +926,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
) : null}
</Animated.View>

<ImageViewing
images={previewImageUri ? [{ uri: previewImageUri }] : []}
imageIndex={0}
visible={previewImageUri !== null}
onRequestClose={closePreview}
swipeToCloseEnabled
doubleTapToZoomEnabled
/>
<FullScreenImageViewer source={previewSource} onRequestClose={closePreview} />
</Animated.View>
);
});
28 changes: 9 additions & 19 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
View,
} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import ImageViewing from "react-native-image-viewing";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
FadeIn,
Expand All @@ -65,6 +64,8 @@ import {

import { AppText as Text } from "../../components/AppText";
import { CopyTextButton } from "../../components/CopyTextButton";
import { FullScreenImageViewer } from "../../components/FullScreenImageViewer";
import { useShareImage } from "../../lib/useShareImage";
import {
parseReviewCommentMessageSegments,
type ReviewInlineComment,
Expand Down Expand Up @@ -176,6 +177,7 @@ function MessageAttachmentImage(props: {
_tag: "attachment",
attachmentId: props.attachmentId,
});
const share = useShareImage();

if (uri === null) {
return (
Expand All @@ -186,7 +188,11 @@ function MessageAttachmentImage(props: {
}

return (
<TouchableOpacity activeOpacity={0.7} onPress={() => props.onPressImage(uri)}>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => props.onPressImage(uri)}
onLongPress={() => share({ uri })}
>
<Image source={{ uri }} className={props.className} resizeMode="cover" />
</TouchableOpacity>
);
Expand Down Expand Up @@ -1916,23 +1922,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
) : null}
</View>

<ImageViewing
images={
expandedImage
? [
{
uri: expandedImage.uri,
headers: expandedImage.headers,
},
]
: []
}
imageIndex={0}
visible={expandedImage !== null}
onRequestClose={() => setExpandedImage(null)}
swipeToCloseEnabled
doubleTapToZoomEnabled
/>
<FullScreenImageViewer source={expandedImage} onRequestClose={() => setExpandedImage(null)} />
</>
);
});
Loading
Loading