From 98428124356a0ceb19f60bd28bd2aa19c20e6b48 Mon Sep 17 00:00:00 2001
From: Eero Salla <28145295+eerosal@users.noreply.github.com>
Date: Thu, 18 Jul 2024 17:41:22 +0300
Subject: [PATCH 1/4] Fix lag when switching between days in the calendar
---
app/_layout.tsx | 9 ++-
components/timetable/Event.tsx | 22 +++++---
elements/timetable/EventsBox.tsx | 65 +++++++++++++++-------
hooks/useFavorite.ts | 95 ++++++++++++++++----------------
package-lock.json | 47 +++++++++++++++-
package.json | 4 +-
6 files changed, 165 insertions(+), 77 deletions(-)
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 6ac37a5..82083fc 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,5 +1,6 @@
import { TabBarIcon } from '@/components';
import { GlobalStateProvider } from '@/hooks/providers/GlobalStateProvider';
+import { useFavoriteStoreStatus } from '@/hooks/useFavorite';
import Locales from '@/locales';
import { Themes } from '@/styles';
import { useFonts } from 'expo-font';
@@ -34,13 +35,15 @@ export default function RootLayout() {
Roboto: require('../assets/fonts/Roboto-Regular.ttf'),
});
+ const { hydrated: favoriteStoreReady } = useFavoriteStoreStatus();
+
const { t } = useTranslation();
useEffect(() => {
- if (loaded) {
- SplashScreen.hideAsync();
+ if (loaded && favoriteStoreReady) {
+ SplashScreen.hideAsync().catch();
}
- }, [loaded]);
+ }, [loaded, favoriteStoreReady]);
if (!loaded) {
return null;
diff --git a/components/timetable/Event.tsx b/components/timetable/Event.tsx
index 67eeebb..5050082 100644
--- a/components/timetable/Event.tsx
+++ b/components/timetable/Event.tsx
@@ -1,7 +1,9 @@
import { useFavorite } from '@/hooks/useFavorite';
import dayjs from 'dayjs';
+import { Image } from 'expo-image';
+import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { Image, View } from 'react-native';
+import { View } from 'react-native';
import { IconButton, Surface, Text } from 'react-native-paper';
interface EventProps {
@@ -26,10 +28,15 @@ const getEventTimeString = (start: Date, end: Date) => {
};
const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps) => {
- const timeString = getEventTimeString(start, end);
- const { favorite, toggle: toggleFavorite } = useFavorite(id);
+ const timeString = useMemo(() => getEventTimeString(start, end), [start, end]);
+ const { isFavorite, setIsFavorite } = useFavorite(id);
const { t, i18n } = useTranslation();
- dayjs.locale(i18n.language);
+
+ const [isAfterEnd, isBeforeStart] = useMemo(() => {
+ dayjs.locale(i18n.language);
+
+ return [dayjs().isAfter(end), dayjs().isBefore(start)];
+ }, [end, i18n.language, start]);
return (
- {dayjs().isAfter(end) &&
+ {isAfterEnd &&
process.env.EXPO_PUBLIC_ENVIRONMENT !== 'development' &&
process.env.EXPO_PUBLIC_ENVIRONMENT !== 'preview' && (
{
+ const [loadedEvents, setLoadedEvents] = React.useState([]);
+
+ const isLoading = loadedEvents !== events;
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setLoadedEvents(events);
+ }, 250);
+
+ return () => clearTimeout(timeout);
+ }, [events]);
+
return (
-
- {events.map((event) => (
-
- ))}
-
+ <>
+ {isLoading ? : null}
+
+ removeClippedSubviews={true}
+ pointerEvents={isLoading ? 'none' : 'auto'}
+ style={{
+ paddingHorizontal: 30,
+ paddingBottom: 8,
+ opacity: isLoading ? 0 : 1,
+ }}
+ renderItem={({ item: event }) => (
+
+ )}
+ contentContainerStyle={{ gap: 8 }}
+ keyExtractor={(event) => event.id.toString()}
+ getItem={(data, index) => data[index]}
+ getItemCount={(_) => loadedEvents.length}
+ data={loadedEvents}
+ initialNumToRender={8}
+ />
+ >
);
};
-export default EventsBox;
+export default React.memo(EventsBox);
diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts
index c514584..745a3ed 100644
--- a/hooks/useFavorite.ts
+++ b/hooks/useFavorite.ts
@@ -1,63 +1,66 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
-import { useEffect, useState } from 'react';
+import { useCallback, useMemo } from 'react';
+import { create } from 'zustand';
+import { createJSONStorage, persist } from 'zustand/middleware';
+import { useShallow } from 'zustand/react/shallow';
-/**
- * Retrieves the favorite events from async storage.
- * @returns A promise that resolves to an array of numbers representing the favorite event ids .
- */
-const getFavorites = async (): Promise => {
- try {
- const data = await AsyncStorage.getItem('favorite_events');
- if (data) {
- return data.split(';').map((n) => parseInt(n));
- }
- } catch (ex) {
- console.error('Failed to get favorites from async storage:', ex);
- }
- return [];
+type FavoriteStore = {
+ hydrated: boolean;
+ setHydrated: (value: boolean) => void;
+ favorites: Record;
+ setFavorite: (id: number, value: boolean) => void;
};
-/**
- * Saves the favorite events to async storage.
- *
- * @param {number[]} favorites - The array of favorite event IDs.
- */
-const saveFavorites = async (favorites: number[]) => {
- try {
- await AsyncStorage.setItem('favorite_events', favorites.join(';'));
- } catch (ex) {
- console.error('Failed to save favorites to async storage:', ex);
- }
-};
+const useFavoriteStore = create()(
+ persist(
+ (set) => ({
+ hydrated: false,
+ setHydrated: (value) => set({ hydrated: value }),
+ favorites: {},
+ setFavorite: (id, value) =>
+ set((state) => ({
+ favorites: { ...state.favorites, [id.toString(10)]: value || undefined },
+ })),
+ }),
+ {
+ name: 'favoriteEvents',
+ onRehydrateStorage: () => (state) => state?.setHydrated(true),
+ storage: createJSONStorage(() => AsyncStorage),
+ }
+ )
+);
/**
* Hook to manage favorite state for a given eventId.
*
* @param id - The ID of the event to track favorite state for.
- * @returns An object containing the current favorite state and a function to toggle the favorite state.
+ * @returns An object containing the current favorite state and a function to set the favorite state.
*/
export const useFavorite = (id: number) => {
- const [favorite, setFavorite] = useState(false);
+ const idStr = id.toString(10);
- useEffect(() => {
- getFavorites().then((favorites) => {
- setFavorite(favorites.includes(id));
- });
- }, [id]);
+ const [isFavorite, setFavorite] = useFavoriteStore(
+ useShallow((state) => {
+ return [state.favorites[idStr] ?? false, state.setFavorite];
+ })
+ );
- const toggle = async () => {
- let favorites = await getFavorites();
+ const setIsFavorite = useCallback(
+ (value: boolean) => {
+ setFavorite(id, value);
+ },
+ [id, setFavorite]
+ );
- if (favorite) {
- favorites.push(id);
- } else {
- favorites.filter((f) => f !== id);
- }
+ return useMemo(() => ({ isFavorite, setIsFavorite }), [isFavorite, setIsFavorite]);
+};
- saveFavorites(favorites);
- // Trigger redraw
- setFavorite(!favorite);
- };
+/**
+ * Hook to get the current favorite store status.
+ * @returns An object containing the current hydration status.
+ */
+export const useFavoriteStoreStatus = () => {
+ const hydrated = useFavoriteStore((state) => state.hydrated);
- return { favorite, toggle };
+ return useMemo(() => ({ hydrated }), [hydrated]);
};
diff --git a/package-lock.json b/package-lock.json
index c114a51..701f348 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"expo-build-properties": "~0.12.3",
"expo-constants": "~16.0.2",
"expo-font": "~12.0.7",
+ "expo-image": "~1.12.13",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.16",
"expo-splash-screen": "~0.27.5",
@@ -34,7 +35,8 @@
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.10",
- "react-native-webview": "13.8.6"
+ "react-native-webview": "13.8.6",
+ "zustand": "^4.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.0",
@@ -9574,6 +9576,14 @@
"expo": "*"
}
},
+ "node_modules/expo-image": {
+ "version": "1.12.13",
+ "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.12.13.tgz",
+ "integrity": "sha512-Dmuc5qmkIsl1nFj8C3Ux3wL2bN4QYW4dM9fkGA8kYiP5Fxf1lT36ldkHk2O2lPFRSFJDvLxT8Tz+7GTko5fzwQ==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-keep-awake": {
"version": "13.0.2",
"license": "MIT",
@@ -17666,6 +17676,14 @@
"react": ">=16.8"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"license": "MIT",
@@ -18184,6 +18202,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz",
+ "integrity": "sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 2e767b8..244549c 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,9 @@
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.10",
- "react-native-webview": "13.8.6"
+ "react-native-webview": "13.8.6",
+ "zustand": "^4.5.4",
+ "expo-image": "~1.12.13"
},
"devDependencies": {
"@babel/core": "^7.20.0",
From 6138aa0e10754aef411e085694175df1bfc6bc76 Mon Sep 17 00:00:00 2001
From: Eero Salla <28145295+eerosal@users.noreply.github.com>
Date: Thu, 18 Jul 2024 17:43:49 +0300
Subject: [PATCH 2/4] Fix Webview background to match the app background
---
app/credits.tsx | 8 ++++++--
elements/about/AboutWebview.tsx | 4 +++-
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/app/credits.tsx b/app/credits.tsx
index 260b1af..1754404 100644
--- a/app/credits.tsx
+++ b/app/credits.tsx
@@ -2,10 +2,14 @@ import { useTranslation } from 'react-i18next';
import { ImageBackground, View } from 'react-native';
import { ScrollView } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function Credits() {
const theme = useTheme();
const { t } = useTranslation();
+
+ const safeArea = useSafeAreaInsets();
+
return (
@@ -66,11 +70,11 @@ export default function Credits() {
Luukas Pörtfors
Otto Laakkonen
- Eero Salla
Onni Linnala
Anto Keinänen
Samu Kupiainen
Miika Tuominen
+ Eero Salla
{
const [loading, setLoading] = useState(true);
const { i18n } = useTranslation();
+ const theme = useTheme();
const uri = `https://assembly.org/${i18n.language}/about`;
const whitelist = [
@@ -32,6 +33,7 @@ const AboutWebview = () => {
whitelistedUrls={whitelist}
style={{
display: loading ? 'none' : 'flex',
+ backgroundColor: theme.colors.background,
}}
onLoad={() => setLoading(false)}
source={{ uri }}
From 366302808db371fe91b8882bed71477dfeed47a2 Mon Sep 17 00:00:00 2001
From: Eero Salla <28145295+eerosal@users.noreply.github.com>
Date: Thu, 18 Jul 2024 17:46:09 +0300
Subject: [PATCH 3/4] Fix date selector to be higher z-indexed than the events
to avoid issues when scaling
---
elements/timetable/Timetable.tsx | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/elements/timetable/Timetable.tsx b/elements/timetable/Timetable.tsx
index 66c7b66..d91b2c0 100644
--- a/elements/timetable/Timetable.tsx
+++ b/elements/timetable/Timetable.tsx
@@ -62,13 +62,19 @@ const Timetable = () => {
) : (
<>
- 0}
- />
+
+ 0}
+ />
+
From 03564097f34ce46b4e819e6155b9378108f6698e Mon Sep 17 00:00:00 2001
From: Eero Salla <28145295+eerosal@users.noreply.github.com>
Date: Thu, 18 Jul 2024 17:47:04 +0300
Subject: [PATCH 4/4] Prevent event titles from flowing under the favorite
button
---
components/timetable/Event.tsx | 73 +++++++++++++++++++++++-----------
1 file changed, 49 insertions(+), 24 deletions(-)
diff --git a/components/timetable/Event.tsx b/components/timetable/Event.tsx
index 5050082..9c03989 100644
--- a/components/timetable/Event.tsx
+++ b/components/timetable/Event.tsx
@@ -94,33 +94,58 @@ const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps
backgroundColor: 'rgba(0, 0, 0, 0.75)',
}}
/>
-
-
- {title}
-
- {location && (
+
+
+
{`${t('location')}: ${location}`}
- )}
-
- {`${t('time')}: ${timeString}`}
-
-
- {dayjs().isBefore(start) ||
- ((process.env.EXPO_PUBLIC_ENVIRONMENT === 'development' ||
- process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && (
- toggleFavorite()}
- icon={favorite ? 'heart' : 'heart-outline'}
+ variant='titleMedium'
style={{
- position: 'absolute',
- top: 0,
- right: 0,
+ textAlign: 'center',
}}
- />
- ))}
+ >
+ {title}
+
+ {location && (
+ {`${t('location')}: ${location}`}
+ )}
+
+ {`${t('time')}: ${timeString}`}
+
+
+ {isBeforeStart ||
+ ((process.env.EXPO_PUBLIC_ENVIRONMENT === 'development' ||
+ process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && (
+ setIsFavorite(!isFavorite)}
+ icon={isFavorite ? 'heart' : 'heart-outline'}
+ style={{
+ height: 40,
+ width: 40,
+ }}
+ />
+ ))}
+
);
};