Skip to content

Commit 495ed81

Browse files
authored
Feat/calendar revival (#3685)
* Increase page size in MockServer and enhance event rendering in CalendarScreen * Enhance Agenda component with useDidUpdate for improved event scrolling and clean up itemHeight prop * Refactor Header component to improve throttle timing and clean up animated props; update AgendaProps to allow null itemHeight * Update date initialization and scrolling behavior in Calendar components * Throttle event loading in CalendarScreen to optimize performance * Increase page size in MockServer and enhance CalendarScreen layout with loading toast * Update background color of Card component in CalendarScreen * Enhance IncubatorCalendarScreen with styled section headers and improve Agenda component's sticky header functionality * expect TS error * Refactor Calendar component to rename state variable from 'items' to 'monthItems' * Refactor event loading logic to prevent multiple simultaneous requests * Update header month title updates and adjust throttling for arrow presses; modify viewability config and years range in calendar * Expose incubator calendar screen * Revert irrelevant changes * Update yarn.lock * Remove itemHeight prop for Agenda * move arrow press throttle options to a const
1 parent 176f887 commit 495ed81

File tree

7 files changed

+121
-73
lines changed

7 files changed

+121
-73
lines changed

demo/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ module.exports = {
260260
get IncubatorExpandableOverlay() {
261261
return require('./screens/incubatorScreens/IncubatorExpandableOverlayScreen').default;
262262
},
263+
get IncubatorCalendarScreen() {
264+
return require('./screens/incubatorScreens/IncubatorCalendarScreen').default;
265+
},
263266
// realExamples
264267
get AppleMusic() {
265268
return require('./screens/realExamples/AppleMusic').default;

demo/src/screens/incubatorScreens/IncubatorCalendarScreen/MockServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _ from 'lodash';
22
import {data} from './MockData';
33

4-
const PAGE_SIZE = 100;
4+
const PAGE_SIZE = 400;
55
const FAKE_FETCH_TIME = 1500;
66

77
class MockServer {
Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import _ from 'lodash';
2+
import {StyleSheet} from 'react-native';
23
import React, {Component} from 'react';
3-
import {Incubator, View, Text} from 'react-native-ui-lib';
4+
import {Incubator, View, Text, Card, Colors} from 'react-native-ui-lib';
45
import MockServer from './MockServer';
56

67
export default class CalendarScreen extends Component {
78
pageIndex = 0;
9+
loadingEventsPromise?: Promise<any[]>;
810

911
state = {
10-
date: new Date().getTime(),
12+
date: new Date(/* '2025-01-12' */).getTime(),
1113
events: [] as any[],
1214
showLoader: false
1315
};
@@ -16,17 +18,25 @@ export default class CalendarScreen extends Component {
1618
this.loadEvents(this.state.date);
1719
}
1820

19-
loadEvents = async (date: number) => {
21+
// Note: we throttle event loading because initially the Agenda reach end and trigger extra event load
22+
loadEvents = (async (date: number) => {
23+
24+
if (this.loadingEventsPromise) {
25+
return;
26+
}
27+
2028
this.setState({showLoader: true});
2129
// const {events} = this.state;
22-
const newEvents = await MockServer.getEvents(date);
30+
this.loadingEventsPromise = MockServer.getEvents(date);
31+
const newEvents = await this.loadingEventsPromise;
32+
this.loadingEventsPromise = undefined;
2333
this.pageIndex++;
2434
// this.setState({events: _.uniqBy([...events, ...newEvents], e => e.id), showLoader: false});
2535
this.setState({events: newEvents, showLoader: false});
26-
};
36+
});
2737

2838
onChangeDate = (date: number) => {
29-
console.log('Date change: ', date);
39+
/* console.log('Date change: ', date); */
3040
const {events} = this.state;
3141
if (date < events[0]?.start || date > _.last(events)?.start) {
3242
console.log('Load new events');
@@ -41,28 +51,37 @@ export default class CalendarScreen extends Component {
4151

4252
// TODO: Fix type once we export them
4353
renderEvent = (eventItem: any) => {
54+
const makeEventBigger = new Date(eventItem.start).getDay() % 2 === 0;
55+
const startTime = new Date(eventItem.start).toLocaleString('en-GB', {
56+
// month: 'short',
57+
// day: 'numeric',
58+
hour12: false,
59+
hour: '2-digit',
60+
minute: '2-digit'
61+
});
62+
const endTime = new Date(eventItem.end).toLocaleString('en-GB', {
63+
hour12: false,
64+
hour: '2-digit',
65+
minute: '2-digit'
66+
});
4467
return (
45-
<View marginH-10 padding-5 bg-blue70>
46-
<Text>
47-
Item for
48-
{new Date(eventItem.start).toLocaleString('en-GB', {
49-
month: 'short',
50-
day: 'numeric',
51-
hour12: false,
52-
hour: '2-digit',
53-
minute: '2-digit'
54-
})}
55-
-{new Date(eventItem.end).toLocaleString('en-GB', {hour12: false, hour: '2-digit', minute: '2-digit'})}
68+
<Card marginH-s5 marginB-s4 padding-s4 backgroundColor={Colors.$backgroundGeneralLight}>
69+
<Text text70>Event Title</Text>
70+
{makeEventBigger && <Text>Event short description</Text>}
71+
<Text marginT-s1 text90>
72+
{startTime}-{endTime}
5673
</Text>
57-
</View>
74+
</Card>
5875
);
5976
};
6077

6178
// TODO: Fix type once we export them
6279
renderHeader = (headerItem: any) => {
6380
return (
64-
<View centerV flex marginL-5>
65-
<Text>{headerItem.header}</Text>
81+
<View bg-$backgroundDefault paddingH-s5 centerV flex paddingV-s2 style={styles.sectionHeader}>
82+
<Text text70BO>
83+
{new Date(headerItem.date).toLocaleString('en-US', {weekday: 'long', day: 'numeric', month: 'short'})}
84+
</Text>
6685
</View>
6786
);
6887
};
@@ -71,15 +90,23 @@ export default class CalendarScreen extends Component {
7190
const {date, events, showLoader} = this.state;
7291

7392
return (
74-
<Incubator.Calendar data={events} initialDate={date} onChangeDate={this.onChangeDate} staticHeader>
75-
<Incubator.Calendar.Agenda
76-
renderEvent={this.renderEvent}
77-
renderHeader={this.renderHeader}
78-
// itemHeight={30}
79-
onEndReached={this.onEndReached}
80-
showLoader={showLoader}
81-
/>
82-
</Incubator.Calendar>
93+
<View flex>
94+
<Incubator.Calendar data={events} initialDate={date} onChangeDate={this.onChangeDate} staticHeader>
95+
<Incubator.Calendar.Agenda
96+
renderEvent={this.renderEvent}
97+
renderHeader={this.renderHeader}
98+
onEndReached={this.onEndReached}
99+
showLoader={showLoader}
100+
/>
101+
</Incubator.Calendar>
102+
<Incubator.Toast visible={showLoader} message="Loading events..." preset="general"/>
103+
</View>
83104
);
84105
}
85106
}
107+
108+
const styles = StyleSheet.create({
109+
sectionHeader: {
110+
opacity: 0.9
111+
}
112+
});

src/incubator/calendar/Agenda.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, {useContext, useCallback, useRef} from 'react';
1+
import React, {useContext, useCallback, useRef, useState} from 'react';
22
import {ActivityIndicator, StyleSheet} from 'react-native';
33
import {runOnJS, useAnimatedReaction, useSharedValue} from 'react-native-reanimated';
44
import {FlashListPackage} from 'optionalDeps';
55
import type {FlashList as FlashListType, ViewToken} from '@shopify/flash-list';
66
import {BorderRadiuses, Colors} from 'style';
7+
import {useDidUpdate} from 'hooks';
78
import View from '../../components/view';
89
import Text from '../../components/text';
910
import {isSameDay, isSameMonth} from './helpers/DateUtils';
@@ -12,29 +13,39 @@ import CalendarContext from './CalendarContext';
1213

1314
const FlashList = FlashListPackage?.FlashList;
1415

15-
// TODO: Fix initial scrolling
1616
function Agenda(props: AgendaProps) {
17-
const {renderEvent, renderHeader, itemHeight = 50, onEndReached, showLoader} = props;
17+
const {renderEvent, renderHeader, onEndReached, showLoader} = props;
1818
const {data, selectedDate, setDate, updateSource} = useContext(CalendarContext);
1919
const flashList = useRef<FlashListType<InternalEvent>>(null);
2020
const closestSectionHeader = useSharedValue<DateSectionHeader | null>(null);
2121
const scrolledByUser = useSharedValue<boolean>(false);
22+
const [stickyHeaderIndices, setStickyHeaderIndices] = useState<number[]>([]);
23+
const lastDateBeforeLoadingNewEvents = useSharedValue<number>(selectedDate.value);
2224

2325
/* const keyExtractor = useCallback((item: InternalEvent) => {
2426
return item.type === 'Event' ? item.id : item.header;
2527
}, []); */
2628

29+
useDidUpdate(() => {
30+
const result = findClosestDateAfter(lastDateBeforeLoadingNewEvents.value);
31+
if (result?.index) {
32+
setTimeout(() => scrollToIndex(result?.index, false), 200);
33+
}
34+
35+
const headerIndices = data
36+
.map((e, index) => (e.type === 'Header' ? index : undefined))
37+
.filter(i => i !== undefined);
38+
// @ts-expect-error
39+
setStickyHeaderIndices(headerIndices);
40+
}, [data]);
41+
2742
const _renderEvent = useCallback((eventItem: Event) => {
2843
if (renderEvent) {
29-
return (
30-
<View height={itemHeight} style={styles.eventContainer}>
31-
{renderEvent(eventItem)}
32-
</View>
33-
);
44+
return <View style={styles.eventContainer}>{renderEvent(eventItem)}</View>;
3445
}
3546

3647
return (
37-
<View marginV-1 marginH-10 paddingH-10 height={itemHeight} centerV style={styles.event}>
48+
<View marginV-1 marginH-10 paddingH-10 centerV style={styles.event}>
3849
<Text>
3950
Item for
4051
{new Date(eventItem.start).toLocaleString('en-GB', {
@@ -49,20 +60,20 @@ function Agenda(props: AgendaProps) {
4960
</View>
5061
);
5162
},
52-
[renderEvent, itemHeight]);
63+
[renderEvent]);
5364

5465
const _renderHeader = useCallback((headerItem: DateSectionHeader) => {
5566
if (renderHeader) {
56-
return <View height={itemHeight}>{renderHeader(headerItem)}</View>;
67+
return <View>{renderHeader(headerItem)}</View>;
5768
}
5869

5970
return (
60-
<View bottom marginB-5 marginH-20 height={itemHeight}>
71+
<View bg-$backgroundDefault bottom marginB-5 marginH-20>
6172
<Text>{headerItem.header}</Text>
6273
</View>
6374
);
6475
},
65-
[renderHeader, itemHeight]);
76+
[renderHeader]);
6677

6778
const renderItem = useCallback(({item}: {item: InternalEvent; index: number}) => {
6879
switch (item.type) {
@@ -114,6 +125,10 @@ function Agenda(props: AgendaProps) {
114125
const _isSameMonth = isSameMonth(selected, previous);
115126
runOnJS(scrollToIndex)(index, _isSameMonth);
116127
}
128+
} else {
129+
// Note: We got here because we are missing future agenda events to scroll to.
130+
// therefor we should expect and new events data load
131+
lastDateBeforeLoadingNewEvents.value = selectedDate.value;
117132
}
118133
}
119134
}
@@ -146,6 +161,7 @@ function Agenda(props: AgendaProps) {
146161
}, []);
147162

148163
const _onEndReached = useCallback(() => {
164+
lastDateBeforeLoadingNewEvents.value = selectedDate.value;
149165
onEndReached?.(selectedDate.value);
150166
// eslint-disable-next-line react-hooks/exhaustive-deps
151167
}, [onEndReached]);
@@ -160,6 +176,7 @@ function Agenda(props: AgendaProps) {
160176
// keyExtractor={keyExtractor}
161177
renderItem={renderItem}
162178
getItemType={getItemType}
179+
stickyHeaderIndices={stickyHeaderIndices}
163180
onViewableItemsChanged={onViewableItemsChanged}
164181
onMomentumScrollBegin={onMomentumScrollBegin}
165182
onScrollBeginDrag={onScrollBeginDrag}

src/incubator/calendar/Header.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {HeaderProps, DayNamesFormat, UpdateSource} from './types';
1010
import CalendarContext from './CalendarContext';
1111
import WeekDaysNames from './WeekDaysNames';
1212

13+
// Note: this fixes the updates on the header month title
14+
Reanimated.addWhitelistedNativeProps({text: true});
1315

16+
const ARROWS_THROTTLE_TIME = 300;
17+
const ARROWS_THROTTLE_OPTIONS = {leading: true, trailing: false};
1418
const WEEK_NUMBER_WIDTH = 32;
1519
const ARROW_NEXT = require('./assets/arrowNext.png');
1620
const ARROW_BACK = require('./assets/arrowBack.png');
@@ -28,11 +32,13 @@ const Header = (props: HeaderProps) => {
2832

2933
const onLeftArrowPress = useCallback(throttle(() => {
3034
setDate(getNewDate(-1), UpdateSource.MONTH_ARROW);
31-
}, 300), [setDate, getNewDate]);
35+
}, ARROWS_THROTTLE_TIME, ARROWS_THROTTLE_OPTIONS),
36+
[setDate, getNewDate]);
3237

3338
const onRightArrowPress = useCallback(throttle(() => {
3439
setDate(getNewDate(1), UpdateSource.MONTH_ARROW);
35-
}, 300), [setDate, getNewDate]);
40+
}, ARROWS_THROTTLE_TIME, ARROWS_THROTTLE_OPTIONS),
41+
[setDate, getNewDate]);
3642

3743
const getTitle = useCallback((date: number) => {
3844
'worklet';
@@ -42,15 +48,17 @@ const Header = (props: HeaderProps) => {
4248
return getMonthForIndex(m) + ` ${y}`;
4349
}, []);
4450

45-
const animatedProps = useAnimatedProps(() => { // get called only on value update
51+
const animatedProps = useAnimatedProps(() => {
52+
// get called only on value update
4653
return {
4754
text: getTitle(selectedDate.value)
4855
};
4956
});
5057

5158
const onLayout = useCallback((event: LayoutChangeEvent) => {
5259
setHeaderHeight?.(event.nativeEvent.layout.height);
53-
}, [setHeaderHeight]);
60+
},
61+
[setHeaderHeight]);
5462

5563
const renderTitle = () => {
5664
if (!staticHeader) {
@@ -59,23 +67,17 @@ const Header = (props: HeaderProps) => {
5967
}
6068
return (
6169
//@ts-expect-error - hack to animate the title text change
62-
<AnimatedTextInput
70+
<AnimatedTextInput
6371
value={getTitle(selectedDate.value)} // setting initial value
6472
{...{animatedProps}}
6573
editable={false}
6674
style={styles.title}
67-
/>);
75+
/>
76+
);
6877
};
6978

7079
const renderArrow = (source: number, onPress: () => void) => {
71-
return (
72-
<Button
73-
link
74-
size={Button.sizes.xSmall}
75-
iconSource={source}
76-
onPress={onPress}
77-
/>
78-
);
80+
return <Button link size={Button.sizes.xSmall} iconSource={source} onPress={onPress}/>;
7981
};
8082

8183
const renderNavigation = () => {

0 commit comments

Comments
 (0)