Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lightbox mobile #2470

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
33 changes: 18 additions & 15 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,70 +17,73 @@
"@react-navigation/material-top-tabs": "^6.6.0",
"@react-navigation/native": "^6.1.4",
"@react-navigation/native-stack": "^6.9.10",
"@sentry/react-native": "5.19.1",
"@sentry/react-native": "~5.20.0",
"@shopify/flash-list": "1.6.3",
"@types/node": "20.11.16",
"@walletconnect/modal-react-native": "^1.0.0-rc.10",
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
"ethers": "6.11.1",
"expo": "^50.0.17",
"expo-application": "~5.8.3",
"expo-application": "~5.8.4",
"expo-asset": "~9.0.2",
"expo-av": "~13.10.5",
"expo-av": "~13.10.6",
"expo-barcode-scanner": "~12.9.3",
"expo-blur": "~12.9.2",
"expo-build-properties": "^0.11.1",
"expo-constants": "~15.4.5",
"expo-constants": "~15.4.6",
"expo-crypto": "~12.8.1",
"expo-dev-client": "~3.3.9",
"expo-device": "~5.9.3",
"expo-file-system": "~16.0.8",
"expo-dev-client": "~3.3.11",
"expo-device": "~5.9.4",
"expo-file-system": "~16.0.9",
"expo-font": "~11.10.3",
"expo-linear-gradient": "^12.7.2",
"expo-linking": "~6.2.2",
"expo-notifications": "~0.27.6",
"expo-notifications": "~0.27.7",
"expo-secure-store": "~12.8.1",
"expo-splash-screen": "~0.26.4",
"expo-splash-screen": "~0.26.5",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
"expo-updates": "~0.24.11",
"expo-system-ui": "~2.9.4",
"expo-updates": "~0.24.12",
"expo-web-browser": "~12.8.2",
"lodash.merge": "^4.6.2",
"lru-cache": "^9.1.1",
"mixpanel-react-native": "^3.0.0-beta.2",
"nativewind": "^2.0.11",
"node-html-parser": "^6.1.5",
"react": "18.2.0",
"react-native": "0.73.4",
"react": "18.2.45",
"react-native": "0.73.6",
"react-native-collapsible-tab-view": "^6.1.4",
"react-native-fast-image": "^8.6.3",
"react-native-fetch-api": "^3.0.0",
"react-native-gesture-handler": "^2.15.0",
"react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^2.2.0",
"react-native-ios-context-menu": "^1.15.3",
"react-native-lightbox-v2": "^0.9.0",
"react-native-linear-gradient": "^2.6.2",
"react-native-markdown-display": "https://github.com/jonasmerlin/react-native-markdown-display.git",
"react-native-mmkv": "^2.12.2",
"react-native-modal": "^13.0.1",
"react-native-pager-view": "6.2.3",
"react-native-polyfill-globals": "^3.1.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-reanimated": "^3.8.1",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-skeleton-placeholder": "^5.2.4",
"react-native-svg": "14.1.0",
"react-native-tab-view": "^3.4.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-webview": "13.6.4",
"react-native-zoom-reanimated": "^1.4.5",
"rfdc": "^1.3.1",
"sentry-expo": "~7.2.0",
"siwe": "^2.1.4",
"swr": "^2.1.1",
"text-encoding": "^0.7.0",
"typescript": "^5.1.3",
"typescript": "^5.3.3",
"viem": "^1.19.11",
"web-streams-polyfill": "^3.2.1",
"zod": "^3.22.3"
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/components/IconContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type IconContainerProps = {
onPress: () => void;
size?: 'xs' | 'sm' | 'md';
border?: boolean;
color?: 'default' | 'white' | 'black' | 'active';
color?: 'default' | 'white' | 'black' | 'faint' | 'active';
} & GalleryTouchableOpacityProps;

export function IconContainer({
Expand All @@ -28,10 +28,11 @@ export function IconContainer({
md: 'h-8 w-8',
};

const colorVariants: { [color in 'default' | 'white' | 'black' | 'active']: string } = {
const colorVariants: { [color in 'default' | 'white' | 'black' | 'faint' | 'active']: string } = {
default: 'bg-faint dark:bg-black-500',
white: 'bg-white dark:bg-black-900',
black: 'bg-black-900 dark:bg-white',
faint: 'bg-black-700',
active: 'bg-porcelain dark:bg-white',
};

Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/icons/CloseIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Svg, { Path, SvgProps } from 'react-native-svg';

export function CloseIcon({ ...props }: SvgProps) {
return (
<Svg width="18" height="18" fill="none" viewBox="0 0 16 16" {...props}>
<Path d="M12.6663 3.33398L3.33301 12.6673" stroke="#F9F9F9" stroke-miterlimit="10" />
<Path d="M3.33301 3.33398L12.6663 12.6673" stroke="#F9F9F9" stroke-miterlimit="10" />
</Svg>
);
}
39 changes: 39 additions & 0 deletions apps/mobile/src/icons/MaximizeIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useColorScheme } from 'nativewind';
import React from 'react';
import Svg, { Path, SvgProps } from 'react-native-svg';

import colors from '~/shared/theme/colors';

export function MaximizeIcon({ ...props }: SvgProps) {
const { colorScheme } = useColorScheme();
const strokeColor = colorScheme === 'dark' ? colors.white : colors.black['800'];

return (
<Svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...props}>
<Path
d="M13.9997 6.66667V2H9.33301"
stroke={strokeColor}
stroke-width="0.666667"
stroke-miterlimit="10"
/>
<Path
d="M13.9997 2L9.33301 6.66667"
stroke={strokeColor}
stroke-width="0.666667"
stroke-miterlimit="10"
/>
<Path
d="M2 9.33398V14.0007H6.66667"
stroke={strokeColor}
stroke-width="0.666667"
stroke-miterlimit="10"
/>
<Path
d="M2 13.9993L6.33333 9.66602"
stroke={strokeColor}
stroke-width="0.666667"
stroke-miterlimit="10"
/>
</Svg>
);
}
140 changes: 130 additions & 10 deletions apps/mobile/src/screens/NftDetailScreen/NftDetailSection.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { useColorScheme } from 'nativewind';
import { useCallback, useMemo } from 'react';
import { ScrollView, View } from 'react-native';
import { useCallback, useMemo, useRef, useState } from 'react';
import { Dimensions, ScrollView, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import Lightbox from 'react-native-lightbox-v2';
import { graphql, useFragment } from 'react-relay';
import { useNavigateToCommunityScreen } from 'src/hooks/useNavigateToCommunityScreen';
import { useToggleTokenAdmire } from 'src/hooks/useToggleTokenAdmire';
import { BookmarkIcon } from 'src/icons/BookmarkIcon';
import { CloseIcon } from 'src/icons/CloseIcon';
import { MaximizeIcon } from 'src/icons/MaximizeIcon';
import { PoapIcon } from 'src/icons/PoapIcon';
import { ShareIcon } from 'src/icons/ShareIcon';

Expand All @@ -22,6 +25,7 @@ import {
CreatorProfilePictureAndUsernameOrAddress,
OwnerProfilePictureAndUsername,
} from '~/components/ProfilePicture/ProfilePictureAndUserOrAddress';
import { useSafeAreaPadding } from '~/components/SafeAreaViewWithPadding';
import { Typography } from '~/components/Typography';
import { NftDetailSectionQueryFragment$key } from '~/generated/NftDetailSectionQueryFragment.graphql';
import { PostIcon } from '~/navigation/MainTabNavigator/PostIcon';
Expand All @@ -41,6 +45,8 @@ type Props = {
queryRef: NftDetailSectionQueryFragment$key;
};

const { width } = Dimensions.get('window');

export function NftDetailSection({ onShare, queryRef }: Props) {
const route = useRoute<RouteProp<MainTabStackNavigatorParamList, 'NftDetail'>>();

Expand Down Expand Up @@ -96,6 +102,7 @@ export function NftDetailSection({ onShare, queryRef }: Props) {
);

const { colorScheme } = useColorScheme();
const [isLightboxOpen, setIsLightboxOpen] = useState(false);

const token = query.tokenById;
const ownerWalletAddress =
Expand All @@ -120,6 +127,8 @@ export function NftDetailSection({ onShare, queryRef }: Props) {
}
}, [navigateToCommunity, tokenDefinition.community]);

const { top } = useSafeAreaPadding();

const handleCreatePost = useCallback(() => {
if (token.dbid) {
navigation.navigate('PostComposer', {
Expand Down Expand Up @@ -162,13 +171,98 @@ export function NftDetailSection({ onShare, queryRef }: Props) {
queryRef: query,
});

const customHeader = useCallback(
(close: () => void) => {
return (
<View
className="flex-row justify-end items-center px-3 bg-black-800"
style={{
paddingTop: top,
}}
>
<IconContainer
color="faint"
icon={<CloseIcon />}
onPress={close}
eventElementId={null}
eventName={null}
eventContext={null}
/>
</View>
);
},
[top]
);

const { contractName } = extractRelevantMetadataFromToken(token);

const blueToDisplay = useMemo(
() => (colorScheme === 'dark' ? 'darkModeBlue' : 'activeBlue'),
[colorScheme]
);

const handleMaximizeToggle = useCallback(() => {
setIsLightboxOpen((currIsLightboxOpen) => !currIsLightboxOpen);
}, []);

const thumbnailRef = useRef<View | null>(null);
const [thumbnailPosition, setThumbnailPosition] = useState({
width: width,
height: width,
x: 0,
y: 0,
});

const updateThumbnailPosition = useCallback(() => {
if (thumbnailRef.current) {
thumbnailRef.current.measure((x, y, w, h, pageX, pageY) => {
setThumbnailPosition({
width: w,
height: h,
x: pageX,
y: pageY,
});
});
}
}, []);

const handleOpenLightbox = useCallback(() => {
setIsLightboxOpen(true);
}, [updateThumbnailPosition]);

const handleCloseLightbox = useCallback(() => {
setIsLightboxOpen(false);
}, []);

const contentStyle = useMemo(
() => ({
width: width * 0.92,
backgroundColor: 'red',
}),
[]
);

const renderContent = useCallback(
() => (
<TokenFailureBoundary tokenRef={token} variant="large">
<NftDetailAssetCacheSwapper cachedPreviewAssetUrl={route.params.cachedPreviewAssetUrl}>
<NftDetailAsset tokenRef={token} />
</NftDetailAssetCacheSwapper>
</TokenFailureBoundary>
),
[token, route.params.cachedPreviewAssetUrl]
);

const tokenOrigin = useMemo(
() => ({
x: thumbnailPosition.x,
y: thumbnailPosition.y,
width: thumbnailPosition.width,
height: thumbnailPosition.height,
}),
[thumbnailPosition]
);

return (
<ScrollView>
<View className="flex flex-col space-y-3 px-4 pb-4">
Expand All @@ -188,14 +282,31 @@ export function NftDetailSection({ onShare, queryRef }: Props) {
/>
</View>

<View className="w-full mb-3">
<TokenFailureBoundary tokenRef={token} variant="large">
<NftDetailAssetCacheSwapper
cachedPreviewAssetUrl={route.params.cachedPreviewAssetUrl}
>
<NftDetailAsset tokenRef={token} />
</NftDetailAssetCacheSwapper>
</TokenFailureBoundary>
<View className="flex justify-between w-full mb-3">
<Lightbox
{...{
isOpen: isLightboxOpen,
onClose: handleCloseLightbox,
onOpen: handleOpenLightbox,
backgroundColor: colors.black['800'],
swipeToDismiss: true,
renderHeader: customHeader,
onLayout: updateThumbnailPosition,
doubleTapZoomEnabled: false,
renderContent: renderContent,
origin: tokenOrigin,
}}
>
<View ref={thumbnailRef}>
<TokenFailureBoundary tokenRef={token} variant="large">
<NftDetailAssetCacheSwapper
cachedPreviewAssetUrl={route.params.cachedPreviewAssetUrl}
>
<NftDetailAsset tokenRef={token} />
</NftDetailAssetCacheSwapper>
</TokenFailureBoundary>
</View>
</Lightbox>
</View>
</View>

Expand All @@ -211,6 +322,15 @@ export function NftDetailSection({ onShare, queryRef }: Props) {
{tokenDefinition.name}
</Typography>
</View>
<GalleryTouchableOpacity
className="p-1"
onPress={handleMaximizeToggle}
eventElementId="NFT Detail Maximize Icon"
eventName="NFT Detail Maximize Icon Pressed"
eventContext={contexts['NFT Detail']}
>
<MaximizeIcon />
</GalleryTouchableOpacity>
</View>
<GalleryTouchableOpacity
eventElementId="NFT Detail Contract Name Pill"
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/utils/useIntervalEffectOnAppForeground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export function useIntervalEffectOnAppForeground(callback: () => void) {
let interval: number;

// Set up the interval when the component mounts
interval = setInterval(callback, 10000);
interval = setInterval(callback, 10000) as unknown as number;

const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
// When the app comes into the foreground, trigger the callback
// and kick off the interval
callback();
interval = setInterval(callback, 10000);
interval = setInterval(callback, 10000) as unknown as number;
} else if (nextAppState === 'background' || nextAppState === 'inactive') {
// When the app goes to the background, clear the interval
if (interval) {
Expand Down
Loading
Loading