fix list and improve cloud functions
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { FlatList, StyleSheet, View, useWindowDimensions } from "react-native";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import PersonaCard from "../cards/PersonaCard/PersonaCard";
|
||||
import { ai } from "../../assets";
|
||||
import { getCreationStageStates } from "../../utils/projectStages";
|
||||
@@ -53,29 +65,207 @@ const FeatureCarousel = ({
|
||||
})),
|
||||
[stageStates],
|
||||
);
|
||||
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const itemHeight = Math.max(windowHeight, 1);
|
||||
const isWeb = Platform.OS === "web";
|
||||
|
||||
const [viewportHeight, setViewportHeight] = useState(() =>
|
||||
Math.max(windowHeight, 1),
|
||||
);
|
||||
|
||||
const updateSnapHeight = useCallback((height) => {
|
||||
if (!height || Number.isNaN(height)) {
|
||||
return;
|
||||
}
|
||||
setViewportHeight((prev) => {
|
||||
if (prev == null || Math.abs(prev - height) > 0.5) {
|
||||
return height;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateSnapHeight(Math.max(windowHeight, 1));
|
||||
}, [updateSnapHeight, windowHeight]);
|
||||
|
||||
const itemHeight = Math.max(viewportHeight, 1);
|
||||
|
||||
const listRef = useRef(null);
|
||||
const pendingScrollRef = useRef(false);
|
||||
const activeIndexRef = useRef(activeIndex);
|
||||
const alignTimeoutRef = useRef(null);
|
||||
const activeIndexRef = useRef(
|
||||
typeof activeIndex === "number" ? activeIndex : 0,
|
||||
);
|
||||
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
||||
|
||||
useEffect(() => {
|
||||
activeIndexRef.current = activeIndex;
|
||||
}, [activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
onActiveIndexChangeRef.current = onActiveIndexChange;
|
||||
}, [onActiveIndexChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof activeIndex === "number") {
|
||||
activeIndexRef.current = activeIndex;
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
const clampIndex = useCallback(
|
||||
(index) => {
|
||||
if (!carouselItems.length) {
|
||||
return 0;
|
||||
}
|
||||
if (index < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (index >= carouselItems.length) {
|
||||
return carouselItems.length - 1;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
[carouselItems.length],
|
||||
);
|
||||
|
||||
const clearPendingAlignment = useCallback(() => {
|
||||
if (alignTimeoutRef.current != null) {
|
||||
clearTimeout(alignTimeoutRef.current);
|
||||
alignTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment]);
|
||||
|
||||
const scrollToIndex = useCallback(
|
||||
(index, animated = true, heightOverride) => {
|
||||
const ref = listRef.current;
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clamped = clampIndex(index);
|
||||
const height =
|
||||
heightOverride && heightOverride > 0 ? heightOverride : itemHeight;
|
||||
|
||||
if (isWeb) {
|
||||
if (!height) {
|
||||
return;
|
||||
}
|
||||
updateSnapHeight(height);
|
||||
try {
|
||||
ref.scrollToOffset({ offset: clamped * height, animated: false });
|
||||
} catch (_error) {
|
||||
// Ignore scroll errors when list is not ready yet.
|
||||
}
|
||||
activeIndexRef.current = clamped;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
pendingScrollRef.current = !!animated;
|
||||
ref.scrollToIndex({ index: clamped, animated });
|
||||
activeIndexRef.current = clamped;
|
||||
} catch (_error) {
|
||||
pendingScrollRef.current = false;
|
||||
}
|
||||
},
|
||||
[clampIndex, isWeb, itemHeight, updateSnapHeight],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
listRef.current == null ||
|
||||
typeof activeIndex !== "number" ||
|
||||
activeIndex < 0 ||
|
||||
activeIndex >= carouselItems.length ||
|
||||
(isWeb && !itemHeight)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
scrollToIndex(activeIndex);
|
||||
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current == null || (isWeb && !itemHeight)) {
|
||||
return;
|
||||
}
|
||||
scrollToIndex(activeIndexRef.current, false);
|
||||
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex]);
|
||||
|
||||
const alignToOffset = useCallback(
|
||||
(offset, layoutHeight) => {
|
||||
const height =
|
||||
layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight;
|
||||
if (!height) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSnapHeight(height);
|
||||
|
||||
const nextIndex = clampIndex(Math.round(offset / height));
|
||||
const hasChanged = nextIndex !== activeIndexRef.current;
|
||||
|
||||
if (hasChanged) {
|
||||
activeIndexRef.current = nextIndex;
|
||||
const callback = onActiveIndexChangeRef.current;
|
||||
if (callback) {
|
||||
callback(nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWeb || hasChanged) {
|
||||
scrollToIndex(nextIndex, !isWeb, height);
|
||||
}
|
||||
},
|
||||
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight],
|
||||
);
|
||||
|
||||
const handleScrollEnd = useCallback(
|
||||
(event) => {
|
||||
clearPendingAlignment();
|
||||
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
|
||||
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
|
||||
alignToOffset(offsetY, layoutHeight);
|
||||
},
|
||||
[alignToOffset, clearPendingAlignment],
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event) => {
|
||||
if (!isWeb) {
|
||||
return;
|
||||
}
|
||||
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
|
||||
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
|
||||
clearPendingAlignment();
|
||||
alignTimeoutRef.current = setTimeout(() => {
|
||||
alignToOffset(offsetY, layoutHeight);
|
||||
alignTimeoutRef.current = null;
|
||||
}, 80);
|
||||
},
|
||||
[alignToOffset, clearPendingAlignment, isWeb],
|
||||
);
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(event) => {
|
||||
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0;
|
||||
if (layoutHeight > 0) {
|
||||
updateSnapHeight(layoutHeight);
|
||||
}
|
||||
},
|
||||
[updateSnapHeight],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback((item) => item.key, []);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index }) => (
|
||||
<PersonaCard item={item} index={index} isLock={item.isLocked} />
|
||||
<PersonaCard
|
||||
item={item}
|
||||
index={index}
|
||||
isLock={item.isLocked}
|
||||
height={itemHeight}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
[itemHeight],
|
||||
);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
@@ -108,26 +298,15 @@ const FeatureCarousel = ({
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
listRef.current == null ||
|
||||
typeof activeIndex !== "number" ||
|
||||
activeIndex < 0 ||
|
||||
activeIndex >= carouselItems.length
|
||||
) {
|
||||
return;
|
||||
const snapOffsets = useMemo(() => {
|
||||
if (!itemHeight || !isWeb) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
pendingScrollRef.current = true;
|
||||
listRef.current.scrollToIndex({ index: activeIndex, animated: true });
|
||||
} catch (_error) {
|
||||
pendingScrollRef.current = false;
|
||||
// Ignore scroll errors when list is not ready yet.
|
||||
}
|
||||
}, [activeIndex, carouselItems.length]);
|
||||
return carouselItems.map((_, index) => index * itemHeight);
|
||||
}, [carouselItems.length, isWeb, itemHeight]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={[styles.container, style]} onLayout={handleLayout}>
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={carouselItems}
|
||||
@@ -137,16 +316,22 @@ const FeatureCarousel = ({
|
||||
viewabilityConfig={viewabilityConfig.current}
|
||||
getItemLayout={getItemLayout}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pagingEnabled
|
||||
pagingEnabled={!isWeb}
|
||||
bounces={false}
|
||||
overScrollMode="never"
|
||||
initialNumToRender={1}
|
||||
maxToRenderPerBatch={2}
|
||||
windowSize={3}
|
||||
scrollEventThrottle={16}
|
||||
snapToAlignment="start"
|
||||
snapToInterval={itemHeight}
|
||||
decelerationRate="fast"
|
||||
snapToAlignment={isWeb ? undefined : "start"}
|
||||
snapToInterval={!isWeb && itemHeight ? itemHeight : undefined}
|
||||
snapToOffsets={snapOffsets}
|
||||
disableIntervalMomentum={!isWeb}
|
||||
decelerationRate={!isWeb ? "fast" : undefined}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
onScroll={isWeb ? handleScroll : undefined}
|
||||
onMomentumScrollEnd={handleScrollEnd}
|
||||
onScrollEndDrag={isWeb ? handleScrollEnd : undefined}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -162,11 +347,4 @@ const styles = StyleSheet.create({
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
cardRight: {
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import GradientButton from "../GradientButton";
|
||||
|
||||
const BUTTON_BLUR_INTENSITY = 20;
|
||||
const ROW_BLUR_INTENSITY = 5;
|
||||
const ROW_BLUR_SELECTED_INTENSITY = 45;
|
||||
|
||||
const defaultFormatDate = (timestamp) => {
|
||||
try {
|
||||
@@ -191,7 +189,6 @@ export default ProjectDropDown;
|
||||
const ProjectRow = ({
|
||||
project,
|
||||
formatDate,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onModify,
|
||||
isTitle = false,
|
||||
@@ -252,13 +249,7 @@ const ProjectRow = ({
|
||||
source={coverUri ? { uri: coverUri } : img.placeholder}
|
||||
style={styles.projectImage}
|
||||
/>
|
||||
<BlurView
|
||||
intensity={
|
||||
isSelected ? ROW_BLUR_SELECTED_INTENSITY : ROW_BLUR_INTENSITY
|
||||
}
|
||||
tint="dark"
|
||||
style={styles.projectInfo}
|
||||
>
|
||||
<BlurView tint="dark" style={styles.projectInfo}>
|
||||
<View style={styles.projectTexts}>
|
||||
<Text style={styles.projectTitle} numberOfLines={1}>
|
||||
{title}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Text, useWindowDimensions, View } from "react-native";
|
||||
import { Platform, Text, useWindowDimensions, View } from "react-native";
|
||||
import { Image } from "expo-image";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { gutters, Palette } from "../../../styles";
|
||||
@@ -7,16 +7,17 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
import palette from "../../../styles/Palette";
|
||||
|
||||
export default function PersonaCard({ item, isLock = false }) {
|
||||
export default function PersonaCard({ item, isLock = false, height }) {
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const baseHeight = Math.max(height ?? windowHeight, 1);
|
||||
const cardHeight = Platform.OS === "web" ? Math.round(baseHeight) : baseHeight;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: windowHeight,
|
||||
marginTop: -20,
|
||||
height: cardHeight,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -18,7 +18,6 @@ export default function PersonaCard({ item, index, isLock = true }) {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: windowHeight,
|
||||
marginTop: 50,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
Reference in New Issue
Block a user