Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

feat: shared layout element transition in channels #2540

Merged
merged 8 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"react-native-mmkv": "2.10.1",
"react-native-screens": "3.27.0",
"react-native-svg": "14.0.0",
"tailwindcss": "3.3.2"
"tailwindcss": "3.3.2",
"framer-motion": "10.16.5"
},
"react-native": {
"zlib": "browserify-zlib",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useMemo, memo } from "react";
import { Platform, StyleSheet, Dimensions } from "react-native";
import { useMemo } from "react";

import Animated, { AnimatedRef, AnimatedStyle } from "react-native-reanimated";

import { Image } from "@showtime-xyz/universal.image";
import { LightBox } from "@showtime-xyz/universal.light-box";
import { Pressable } from "@showtime-xyz/universal.pressable";
import { useRouter } from "@showtime-xyz/universal.router";

import { ChannelMessageAttachment } from "../types";
import { ChannelMessage, ChannelMessageAttachment } from "../types";
import { LeanText, LeanView } from "./lean-text";

const width = Dimensions.get("window").width;
const height = Dimensions.get("window").height;
const AnimatedImage = Animated.createAnimatedComponent(Image);

const getImageAttachmentWidth = (attachment: ChannelMessageAttachment) => {
export const getImageAttachmentWidth = (
attachment: ChannelMessageAttachment
) => {
if (!attachment || !attachment.height || !attachment.width) {
return 0;
}
Expand All @@ -23,7 +26,9 @@ const getImageAttachmentWidth = (attachment: ChannelMessageAttachment) => {
}
};

const getImageAttachmentHeight = (attachment: ChannelMessageAttachment) => {
export const getImageAttachmentHeight = (
attachment: ChannelMessageAttachment
) => {
if (!attachment || !attachment.height || !attachment.width) {
return 0;
}
Expand All @@ -36,101 +41,69 @@ const getImageAttachmentHeight = (attachment: ChannelMessageAttachment) => {
}
};

export const ImagePreview = memo(
({
attachment,
isViewable,
}: {
attachment: ChannelMessageAttachment;
isViewable?: boolean;
}) => {
const imageAttachmentWidth = useMemo(
() => getImageAttachmentWidth(attachment),
[attachment]
);

const imageAttachmentHeight = useMemo(
() => getImageAttachmentHeight(attachment),
[attachment]
);
const isLandscape =
attachment.width && attachment.height
? attachment.width > attachment.height
: false;

const imageWidth = isLandscape
? Math.max(380, Math.min(width, height * 0.7))
: Math.min(width, height * 0.7);
export const ImagePreview = ({
attachment,
isViewable = false,
animatedRef,
style,
}: {
attachment: ChannelMessage;
isViewable?: boolean;
animatedRef?: AnimatedRef<any>;
style?: AnimatedStyle;
}) => {
const router = useRouter();
const fileObj = useMemo(
() => attachment.attachments[0],
[attachment.attachments]
);
const width = useMemo(() => getImageAttachmentWidth(fileObj), [fileObj]);
const height = useMemo(() => getImageAttachmentHeight(fileObj), [fileObj]);

const imageHeight =
attachment.height && attachment?.width
? isLandscape
? imageWidth * (attachment.height / attachment.width)
: Math.min(
height,
imageWidth * (attachment.height / attachment.width)
)
: 320;

return (
<LeanView
tw="overflow-hidden rounded-xl bg-gray-600"
style={{
width: imageAttachmentWidth,
height: imageAttachmentHeight,
return (
<>
<Pressable
onPress={() => {
router.push(
`/viewer?tag=${attachment?.id}&url=${fileObj.url}&width=${width}&height=${height}`
);
}}
pointerEvents={isViewable ? "auto" : "none"}
disabled={!isViewable}
>
<LightBox
width={imageAttachmentWidth}
height={imageAttachmentHeight}
imgLayout={{
width: "100%",
height:
Platform.OS === "web"
? imageAttachmentHeight
: width *
(attachment.height && attachment?.width
? attachment?.height / attachment.width
: 320),
<AnimatedImage
ref={animatedRef}
tw="web:cursor-pointer"
transition={100}
recyclingKey={attachment.attachments[0]?.media_upload}
width={width}
height={height}
source={{
uri: fileObj.url
? `${fileObj.url}?optimizer=image&width=300`
: undefined,
width: 600,
}}
tapToClose
borderRadius={12}
containerStyle={
Platform.OS === "web"
? {
width: imageWidth,
height: imageHeight,
}
: null
}
alt=""
style={[
{ borderRadius: 8 },
{ backgroundColor: "#f5f5f5" },
{ display: isViewable ? undefined : "none" },
style,
]}
/>
</Pressable>
{isViewable ? null : (
<LeanView
tw="items-center justify-center rounded-lg bg-gray-800 bg-opacity-90"
style={{ width, height }}
>
<Image
tw="web:cursor-pointer"
transition={100}
recyclingKey={attachment?.media_upload}
source={
attachment?.url
? `${attachment?.url}?optimizer=image&width=600`
: undefined
}
alt=""
resizeMode="cover"
style={{
...StyleSheet.absoluteFillObject,
}}
/>
</LightBox>
{!isViewable ? (
<LeanView tw="absolute bottom-0 left-0 right-0 top-0 items-center justify-center bg-gray-800 bg-opacity-90">
<LeanText tw="text-center text-lg text-white dark:text-gray-300">
Unlock to view
</LeanText>
</LeanView>
) : null}
</LeanView>
);
}
);
<LeanText tw="text-center text-lg text-white dark:text-gray-300">
Unlock to view
</LeanText>
</LeanView>
)}
</>
);
};

ImagePreview.displayName = "ImagePreview";
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { useMemo, useState, useRef } from "react";
import { Modal, useWindowDimensions, ScaledSize } from "react-native";

import { motion, useDomEvent } from "framer-motion";

import { Close } from "@showtime-xyz/universal.icon";
import { Text } from "@showtime-xyz/universal.text";
import { View } from "@showtime-xyz/universal.view";

import { ChannelMessage, ChannelMessageAttachment } from "../types";

type SizeProps = {
attachment: ChannelMessageAttachment;
dimensions?: ScaledSize;
};
export const getImageAttachmentWidth = ({
attachment,
dimensions,
}: SizeProps) => {
if (!attachment || !attachment.height || !attachment.width) {
return 0;
}

const aspectRatio = attachment.width / attachment.height;
const maxWidth = !dimensions ? 320 : dimensions.width;
const maxHeight = !dimensions ? 284 : dimensions.height - 150;

// Determine which dimension is the limiting factor (width or height)
if (maxWidth / aspectRatio <= maxHeight) {
// Width is the limiting factor
return maxWidth;
} else {
// Height is the limiting factor
return Math.round(maxHeight * aspectRatio);
}
};

export const getImageAttachmentHeight = ({
attachment,
dimensions,
}: SizeProps) => {
if (!attachment || !attachment.height || !attachment.width) {
return 0;
}

const aspectRatio = attachment.width / attachment.height;
const maxWidth = !dimensions ? 320 : dimensions.width;
const maxHeight = !dimensions ? 284 : dimensions.height - 150;

// Determine which dimension is the limiting factor (width or height)
if (maxHeight * aspectRatio <= maxWidth) {
// Height is the limiting factor
return maxHeight;
} else {
// Width is the limiting factor
return Math.round(maxWidth / aspectRatio);
}
};

const transition = {
type: "spring",
damping: 50,
stiffness: 450,
};

export const ImagePreview = ({
attachment,
isViewable = true,
}: {
attachment: ChannelMessage;
isViewable?: boolean;
}) => {
const [isOpen, setOpen] = useState(false);

useDomEvent(
useRef(document.documentElement),
"scroll",
() => isOpen && setOpen(false)
);

const fileObj = useMemo(
() => attachment.attachments[0],
[attachment.attachments]
);
const dimensions = useWindowDimensions();
const width = useMemo(
() => getImageAttachmentWidth({ attachment: fileObj }),
[fileObj]
);
const height = useMemo(
() => getImageAttachmentHeight({ attachment: fileObj }),
[fileObj]
);

const modalWidth = useMemo(
() => getImageAttachmentWidth({ attachment: fileObj, dimensions }),
[fileObj, dimensions]
);
const modalHeight = useMemo(
() => getImageAttachmentHeight({ attachment: fileObj, dimensions }),
[fileObj, dimensions]
);

return (
<>
<motion.img
src={`${fileObj.url}?optimizer=image&width=300&quality=50`}
alt=""
onClick={() => setOpen((current) => !current)}
layoutId={attachment.id.toString()}
style={{
borderRadius: 8,
position: "relative",
width,
height,
display: isViewable ? "flex" : "none",
cursor: "zoom-in",
transform: "translateZ(0)",
backfaceVisibility: "hidden",
}}
width={width}
height={height}
transition={transition}
draggable={false}
/>

<Modal
transparent
visible={isOpen}
animationType="none"
onRequestClose={() => setOpen((current) => !current)}
>
<View tw="absolute bottom-0 left-0 right-0 top-0 items-center justify-center">
<View
tw="absolute left-5 top-5 cursor-pointer"
onPointerUp={() => setOpen(false)}
>
<Close color="white" width={30} height={30} />
</View>
<motion.div
animate={{ opacity: isOpen ? 0.95 : 0 }}
onClick={() => setOpen(false)}
style={{
width: "100%",
height: "100%",
backgroundColor: "black",
position: "absolute",
opacity: 0,
zIndex: -1,
cursor: "zoom-out",
}}
layoutId="overlay"
/>
<motion.img
src={`${fileObj.url}?optimizer=image&width=1200`}
alt=""
onClick={() => setOpen((current) => !current)}
layoutId={attachment.id.toString()}
style={{
borderRadius: 0,
width: modalWidth,
height: modalHeight,
cursor: "zoom-out",
willChange: "translateZ(0)",
backfaceVisibility: "hidden",
}}
placeholder={`${fileObj.url}?optimizer=image&width=300&quality=50`}
transition={transition}
draggable={false}
/>
</View>
</Modal>
{isViewable ? null : (
<View
tw="items-center justify-center rounded-lg bg-gray-800 bg-opacity-90"
style={{ width, height }}
>
<Text tw="text-center text-lg text-white dark:text-gray-300">
Unlock to view
</Text>
</View>
)}
</>
);
};

ImagePreview.displayName = "ImagePreview";
Loading
Loading