Skip to content

Commit 5cc98cf

Browse files
committed
feat: Chat loader
1 parent 4d0b42d commit 5cc98cf

17 files changed

+324
-123
lines changed

components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { LinearGradient } from "@/design-system/linear-gradient";
12
import { useAppTheme } from "@/theme/useAppTheme";
23
import { hexToRGBA } from "@/utils/colors";
34
import { useSnackbars } from "@components/Snackbar/Snackbar.service";
45
import { useGradientHeight } from "@components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils";
56
// import MaskedView from "@react-native-masked-view/masked-view";
67
import { SICK_SPRING_CONFIG } from "@theme/animations";
7-
import { LinearGradient } from "expo-linear-gradient";
88
import { memo } from "react";
99
import { StyleSheet, useWindowDimensions } from "react-native";
1010
import Animated, {

design-system/Header/Header.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ITextProps, Text } from "../Text";
1212
import { ITouchableOpacityProps } from "../TouchableOpacity";
1313
import { VStack } from "../VStack";
1414
import { HeaderAction } from "./HeaderAction";
15+
import { useHeaderHeight } from "@/design-system/Header/Header.utils";
1516

1617
export type HeaderProps = {
1718
titleStyle?: StyleProp<TextStyle>;
@@ -78,6 +79,8 @@ export function Header(props: HeaderProps) {
7879

7980
const titleContent = titleTx ? translate(titleTx, titleTxOptions) : title;
8081

82+
const headerHeight = useHeaderHeight();
83+
8184
return (
8285
<VStack
8386
// {...debugBorder()}
@@ -90,7 +93,7 @@ export function Header(props: HeaderProps) {
9093
>
9194
<HStack
9295
// {...debugBorder("yellow")}
93-
style={[themed($wrapper), $styleOverride]}
96+
style={[themed($wrapper), $styleOverride, { height: headerHeight }]}
9497
>
9598
<HStack
9699
// {...debugBorder("red")}
@@ -155,7 +158,6 @@ export function Header(props: HeaderProps) {
155158
}
156159

157160
const $wrapper: ThemedStyle<ViewStyle> = ({ spacing }) => ({
158-
height: 72,
159161
alignItems: "center",
160162
paddingHorizontal: spacing.xs,
161163
});

design-system/Header/Header.utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function useHeaderHeight() {
2+
// For now we hardcode it since it's value from Figma.
3+
// Later we might add diff values depending on screen size etc
4+
return 72;
5+
}

design-system/Icon/Icon.android.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const iconRegistry: Record<
4141
block: "block",
4242
"chevron.left": "chevron-left",
4343
"chevron.right": "chevron-right",
44+
"chevron.up": "expand-less",
4445
"arrow.right.circle.fill": "arrow-circle-right",
4546
"lock.open.laptopcomputer": "laptop",
4647
trash: "delete",

design-system/Icon/Icon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const iconRegistry: Record<IIconName, string> = {
3838
block: "nosign",
3939
"chevron.left": "chevron.left",
4040
"chevron.right": "chevron.right",
41+
"chevron.up": "chevron.up",
4142
"arrow.right.circle.fill": "arrow.right.circle.fill",
4243
"lock.open.laptopcomputer": "lock.open.laptopcomputer",
4344
trash: "trash",

design-system/Icon/Icon.types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { ColorValue, StyleProp, TextStyle, ViewStyle } from "react-native";
1+
import { ColorValue, StyleProp, ViewStyle } from "react-native";
22
import { RequireExactlyOne } from "../../types/general";
3-
import { iconRegistry } from "@/design-system/Icon/Icon";
43

54
export type IIconName =
65
| "xmark"
@@ -35,13 +34,14 @@ export type IIconName =
3534
| "block"
3635
| "chevron.left"
3736
| "chevron.right"
37+
| "chevron.up"
38+
| "chevron.down"
3839
| "arrow.right.circle.fill"
3940
| "lock.open.laptopcomputer"
4041
| "trash"
4142
| "menu"
4243
| "more_vert"
4344
| "phone"
44-
| "chevron.down"
4545
| "photo"
4646
| "dollarsign"
4747
| "gear"

design-system/Text/Text.presets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export const textPresets: Record<IPresets, ThemedStyleArray<TextStyle>> = {
2424

2525
body: [textBaseStyle],
2626

27-
bodyBold: [textBaseStyle, textFontWeightStyles.bold],
27+
bodyBold: [textBaseStyle, textFontWeightStyles.medium],
2828

2929
small: [textBaseStyle, textSizeStyles.xs],
3030

3131
smaller: [textBaseStyle, textSizeStyles.xxs],
3232

3333
smallerBold: [textBaseStyle, textSizeStyles.xxs, textFontWeightStyles.bold],
3434

35-
bigBold: [textBaseStyle, textFontWeightStyles.bold],
35+
bigBold: [textBaseStyle, textFontWeightStyles.semiBold],
3636

3737
formHelper: [
3838
textBaseStyle,

design-system/linear-gradient.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { memo } from "react";
77
type ILinearGradientProps = ExpoLinearGradientProps & {
88
isAbsoluteFill?: boolean;
99
orientation?: "vertical" | "horizontal";
10+
debug?: boolean;
1011
};
1112

1213
export const LinearGradient = memo(function LinearGradient(
@@ -15,7 +16,7 @@ export const LinearGradient = memo(function LinearGradient(
1516
const {
1617
isAbsoluteFill,
1718
orientation = "vertical",
18-
19+
debug = false,
1920
colors,
2021
...rest
2122
} = props;
@@ -26,10 +27,12 @@ export const LinearGradient = memo(function LinearGradient(
2627
const end =
2728
rest.end ?? (orientation === "vertical" ? { x: 0, y: 1 } : { x: 1, y: 0 });
2829

30+
const debugColors = ["#0000FF", "#FF0000"];
31+
2932
return (
3033
<ExpoLinearGradient
3134
{...rest}
32-
colors={colors}
35+
colors={debug ? debugColors : colors}
3336
start={start}
3437
end={end}
3538
style={[
Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { AnimatedVStack } from "@/design-system/VStack";
1+
import { AnimatedVStack, VStack } from "@/design-system/VStack";
2+
import { LinearGradient } from "@/design-system/linear-gradient";
23
import { ConversationListItem } from "@/features/conversation-list/conversation-list-item/conversation-list-item";
34
import { ConversationListItemAvatarSkeleton } from "@/features/conversation-list/conversation-list-item/conversation-list-item-avatar-skeleton";
5+
import { useConversationListStyles } from "@/features/conversation-list/conversation-list.styles";
46
import { usePinnedConversations } from "@/features/conversation-list/hooks/use-pinned-conversations";
57
import { useAppTheme } from "@/theme/useAppTheme";
8+
import { hexToRGBA } from "@/utils/colors";
69
import React, { memo } from "react";
710

811
export const ConversationListEmpty = memo(function ConversationListEmpty() {
@@ -19,20 +22,39 @@ export const ConversationListEmpty = memo(function ConversationListEmpty() {
1922
const ConversationListSkeletons = memo(function ConversationListSkeletons() {
2023
const { theme } = useAppTheme();
2124

25+
const { listItemPaddingVertical } = useConversationListStyles();
26+
2227
return (
23-
<AnimatedVStack entering={theme.animation.reanimatedFadeInSpring}>
24-
{/* 8 to fill up the screen */}
25-
{new Array(8).fill(null).map((_, index) => (
26-
<ConversationListItem
27-
key={index}
28-
avatarComponent={
29-
<ConversationListItemAvatarSkeleton
30-
color={theme.colors.fill.minimal}
31-
size={theme.avatarSize.lg}
32-
/>
33-
}
34-
/>
35-
))}
36-
</AnimatedVStack>
28+
<VStack style={{ flex: 1 }}>
29+
<LinearGradient
30+
colors={[
31+
hexToRGBA(theme.colors.background.surface, 0),
32+
hexToRGBA(theme.colors.background.surface, 1),
33+
]}
34+
locations={[0, 0.3]} // 0.3 is trial and error until it looks good
35+
style={{
36+
position: "absolute",
37+
top: (theme.avatarSize.lg + listItemPaddingVertical) * 2, // We want to show 2 full rows before starting the gradient
38+
left: 0,
39+
right: 0,
40+
bottom: 0,
41+
zIndex: 1,
42+
}}
43+
/>
44+
<AnimatedVStack entering={theme.animation.reanimatedFadeInSpring}>
45+
{/* 6 to fill up the screen */}
46+
{new Array(6).fill(null).map((_, index) => (
47+
<ConversationListItem
48+
key={index}
49+
avatarComponent={
50+
<ConversationListItemAvatarSkeleton
51+
color={theme.colors.fill.minimal}
52+
size={theme.avatarSize.lg}
53+
/>
54+
}
55+
/>
56+
))}
57+
</AnimatedVStack>
58+
</VStack>
3759
);
3860
});

features/conversation-list/conversation-list-item/conversation-list-item-avatar-skeleton.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { Center } from "@/design-system/Center";
2-
import { LinearGradient } from "@/design-system/linear-gradient";
3-
import { useAppTheme } from "@/theme/useAppTheme";
4-
import { hexToRGBA } from "@/utils/colors";
52
import { memo } from "react";
63

74
export const ConversationListItemAvatarSkeleton = memo(
@@ -10,7 +7,6 @@ export const ConversationListItemAvatarSkeleton = memo(
107
size: number;
118
}) {
129
const { color, size } = props;
13-
const { theme } = useAppTheme();
1410

1511
return (
1612
<Center
@@ -22,7 +18,8 @@ export const ConversationListItemAvatarSkeleton = memo(
2218
backgroundColor: color,
2319
}}
2420
>
25-
<LinearGradient
21+
{/* Keep until we're sure of final design */}
22+
{/* <LinearGradient
2623
isAbsoluteFill
2724
style={{
2825
borderRadius: 999,
@@ -33,7 +30,7 @@ export const ConversationListItemAvatarSkeleton = memo(
3330
hexToRGBA(theme.colors.background.surfaceless, 0),
3431
theme.colors.background.surface,
3532
]}
36-
/>
33+
/> */}
3734
</Center>
3835
);
3936
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { AnimatedCenter, Center } from "@/design-system/Center";
2+
import { AnimatedText, Text } from "@/design-system/Text";
3+
import { VStack } from "@/design-system/VStack";
4+
import { Loader } from "@/design-system/loader";
5+
import { ConversationListEmpty } from "@/features/conversation-list/conversation-list-empty";
6+
import { $globalStyles } from "@/theme/styles";
7+
import { ThemedStyle, useAppTheme } from "@/theme/useAppTheme";
8+
import { debugBorder } from "@/utils/debug-style";
9+
import { useHeaderHeight } from "@react-navigation/elements";
10+
import React, { memo } from "react";
11+
import { StyleSheet, View, ViewStyle } from "react-native";
12+
import Animated, {
13+
useAnimatedStyle,
14+
useDerivedValue,
15+
useSharedValue,
16+
withRepeat,
17+
withTiming,
18+
withSequence,
19+
Easing,
20+
} from "react-native-reanimated";
21+
import { useSafeAreaInsets } from "react-native-safe-area-context";
22+
23+
export const ConversationListLoading = memo(function ConversationListLoading() {
24+
const headerHeight = useHeaderHeight();
25+
26+
const insets = useSafeAreaInsets();
27+
28+
const countTextAV = useDerivedValue(() => {
29+
// Format time as "00.00.000"
30+
const timeInMs = Date.now() % 60000; // Get milliseconds within a minute
31+
const seconds = Math.floor(timeInMs / 1000);
32+
const milliseconds = timeInMs % 1000;
33+
34+
return `${String(Math.floor(seconds / 10)).padStart(1, "0")}${
35+
seconds % 10
36+
}.${String(milliseconds).padStart(3, "0")}`;
37+
}, []);
38+
39+
return (
40+
<VStack style={$globalStyles.flex1}>
41+
<ConversationListEmpty />
42+
<VStack
43+
{...debugBorder()}
44+
style={[
45+
$globalStyles.absoluteFill,
46+
{
47+
alignItems: "center",
48+
justifyContent: "center",
49+
// To make sure the loader is centered based on the screen height
50+
bottom: headerHeight + insets.top,
51+
},
52+
]}
53+
>
54+
{/* <Loader /> */}
55+
<ArcLoader />
56+
<Text preset="bodyBold">Hello</Text>
57+
<Text color="secondary" preset="small">
58+
Gathering your messages
59+
</Text>
60+
{/* <AnimatedText text={countTextAV} /> */}
61+
</VStack>
62+
</VStack>
63+
);
64+
});
65+
66+
const ArcLoader = () => {
67+
const { themed } = useAppTheme();
68+
const rotation = useSharedValue(0);
69+
70+
React.useEffect(() => {
71+
const timingConfig = {
72+
duration: 1500, // Slower duration for smoother feel
73+
};
74+
75+
rotation.value = withSequence(
76+
withTiming(0, timingConfig),
77+
withRepeat(
78+
withTiming(360, {
79+
duration: 1500,
80+
// Use bezier curve for smoother animation
81+
easing: Easing.bezier(0.35, 0.7, 0.5, 0.7),
82+
}),
83+
-1, // Infinite loop
84+
false // Don't reverse
85+
)
86+
);
87+
}, [rotation]);
88+
89+
const animatedStyle = useAnimatedStyle(() => ({
90+
transform: [{ rotate: `${rotation.value}deg` }],
91+
}));
92+
93+
return <AnimatedCenter style={[themed($arc), animatedStyle]} />;
94+
};
95+
96+
const $arc: ThemedStyle<ViewStyle> = ({ colors, spacing }) => ({
97+
width: 80,
98+
height: 80,
99+
borderWidth: 8,
100+
borderRadius: 40,
101+
borderColor: "transparent",
102+
borderTopColor: colors.text.primary,
103+
borderLeftColor: colors.text.primary,
104+
borderBottomColor: colors.text.primary,
105+
borderTopLeftRadius: 40,
106+
borderTopRightRadius: 40,
107+
borderBottomLeftRadius: 40,
108+
borderBottomRightRadius: 40,
109+
});

0 commit comments

Comments
 (0)