Change way we detect set updates

Every time we create/update/delete fire a DeviceEventEmitter
event for gym sets. Then we subscribe to the changes in relevant
components.

Also fixed flickering of "No data yet" on graphs page.
This commit is contained in:
Brandon Presley 2023-08-24 17:29:52 +12:00
parent f0d5fc4fa6
commit 185ebd1824
11 changed files with 118 additions and 68 deletions

View File

@ -8,12 +8,17 @@ import {
} from "@react-navigation/native"; } from "@react-navigation/native";
import { format } from "date-fns"; import { format } from "date-fns";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { NativeModules, TextInput, View } from "react-native"; import {
DeviceEventEmitter,
NativeModules,
TextInput,
View,
} from "react-native";
import DocumentPicker from "react-native-document-picker"; import DocumentPicker from "react-native-document-picker";
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper"; import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from "./constants"; import { GYM_SET_CREATED, GYM_SET_UPDATED, MARGIN, PADDING } from "./constants";
import { getNow, setRepo, settingsRepo } from "./db"; import { getNow, setRepo, settingsRepo } from "./db";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
import { HomePageParams } from "./home-page-params"; import { HomePageParams } from "./home-page-params";
@ -65,6 +70,7 @@ export default function EditSet() {
); );
const added = async (value: GymSet) => { const added = async (value: GymSet) => {
DeviceEventEmitter.emit(GYM_SET_CREATED);
startTimer(value.name); startTimer(value.name);
console.log(`${EditSet.name}.add`, { set: value }); console.log(`${EditSet.name}.add`, { set: value });
if (!settings.notify) return; if (!settings.notify) return;
@ -104,6 +110,7 @@ export default function EditSet() {
const saved = await setRepo.save(newSet); const saved = await setRepo.save(newSet);
if (typeof set.id !== "number") return added(saved); if (typeof set.id !== "number") return added(saved);
DeviceEventEmitter.emit(GYM_SET_UPDATED);
if (createdDirty) navigate("Sets", { reset: saved.id }); if (createdDirty) navigate("Sets", { reset: saved.id });
else navigate("Sets", { refresh: saved.id }); else navigate("Sets", { refresh: saved.id });
}; };

View File

@ -5,13 +5,13 @@ import {
useRoute, useRoute,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { View } from "react-native"; import { DeviceEventEmitter, View } from "react-native";
import DocumentPicker from "react-native-document-picker"; import DocumentPicker from "react-native-document-picker";
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper"; import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
import { In } from "typeorm"; import { In } from "typeorm";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from "./constants"; import { GYM_SET_UPDATED, MARGIN, PADDING } from "./constants";
import { setRepo, settingsRepo } from "./db"; import { setRepo, settingsRepo } from "./db";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
import { HomePageParams } from "./home-page-params"; import { HomePageParams } from "./home-page-params";
@ -60,6 +60,7 @@ export default function EditSets() {
if (unit) update.unit = unit; if (unit) update.unit = unit;
if (newImage) update.image = newImage; if (newImage) update.image = newImage;
if (Object.keys(update).length > 0) await setRepo.update(ids, update); if (Object.keys(update).length > 0) await setRepo.update(ids, update);
DeviceEventEmitter.emit(GYM_SET_UPDATED);
navigation.goBack(); navigation.goBack();
}; };

View File

@ -5,12 +5,12 @@ import {
useRoute, useRoute,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { ScrollView, TextInput, View } from "react-native"; import { DeviceEventEmitter, ScrollView, TextInput, View } from "react-native";
import DocumentPicker from "react-native-document-picker"; import DocumentPicker from "react-native-document-picker";
import { Button, Card, TouchableRipple } from "react-native-paper"; import { Button, Card, TouchableRipple } from "react-native-paper";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from "./constants"; import { GYM_SET_UPDATED, MARGIN, PADDING } from "./constants";
import { getNow, planRepo, setRepo, settingsRepo } from "./db"; import { getNow, planRepo, setRepo, settingsRepo } from "./db";
import { fixNumeric } from "./fix-numeric"; import { fixNumeric } from "./fix-numeric";
import { defaultSet } from "./gym-set"; import { defaultSet } from "./gym-set";
@ -58,6 +58,7 @@ export default function EditWorkout() {
image: removeImage ? "" : uri, image: removeImage ? "" : uri,
} }
); );
DeviceEventEmitter.emit(GYM_SET_UPDATED);
await planRepo.query( await planRepo.query(
`UPDATE plans `UPDATE plans
SET workouts = REPLACE(workouts, $1, $2) SET workouts = REPLACE(workouts, $1, $2)

View File

@ -6,13 +6,13 @@ import {
useRoute, useRoute,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { ScrollView, TextInput, View } from "react-native"; import { DeviceEventEmitter, ScrollView, TextInput, View } from "react-native";
import DocumentPicker from "react-native-document-picker"; import DocumentPicker from "react-native-document-picker";
import { Button, Card, TouchableRipple } from "react-native-paper"; import { Button, Card, TouchableRipple } from "react-native-paper";
import { In } from "typeorm"; import { In } from "typeorm";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from "./constants"; import { GYM_SETS_EDITED, MARGIN, PADDING } from "./constants";
import { planRepo, setRepo, settingsRepo } from "./db"; import { planRepo, setRepo, settingsRepo } from "./db";
import { fixNumeric } from "./fix-numeric"; import { fixNumeric } from "./fix-numeric";
import Settings from "./settings"; import Settings from "./settings";
@ -74,6 +74,7 @@ export default function EditWorkouts() {
image: removeImage ? "" : uri, image: removeImage ? "" : uri,
} }
); );
DeviceEventEmitter.emit(GYM_SETS_EDITED);
for (const oldName of params.names) { for (const oldName of params.names) {
await planRepo await planRepo
.createQueryBuilder() .createQueryBuilder()

View File

@ -1,47 +1,64 @@
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { useCallback, useEffect, useState } from "react";
import { import {
NavigationProp, DeviceEventEmitter,
useFocusEffect, EmitterSubscription,
useNavigation, FlatList,
} from "@react-navigation/native"; Image,
import { useCallback, useState } from "react"; } from "react-native";
import { FlatList, Image } from "react-native";
import { List } from "react-native-paper"; import { List } from "react-native-paper";
import { getBestSets } from "./best.service"; import { getBestSets } from "./best.service";
import { LIMIT } from "./constants"; import {
GYM_SET_CREATED,
GYM_SET_DELETED,
GYM_SET_UPDATED,
LIMIT,
} from "./constants";
import { settingsRepo } from "./db"; import { settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader"; import DrawerHeader from "./DrawerHeader";
import { GraphsPageParams } from "./GraphsPage"; import { GraphsPageParams } from "./GraphsPage";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
import Page from "./Page"; import Page from "./Page";
import Settings from "./settings"; import Settings, { SETTINGS } from "./settings";
export default function GraphsList() { export default function GraphsList() {
const [bests, setBests] = useState<GymSet[]>([]); const [bests, setBests] = useState<GymSet[]>();
const [refreshing, setRefreshing] = useState(false);
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [end, setEnd] = useState(false); const [end, setEnd] = useState(false);
const [term, setTerm] = useState(""); const [term, setTerm] = useState("");
const navigation = useNavigation<NavigationProp<GraphsPageParams>>(); const navigation = useNavigation<NavigationProp<GraphsPageParams>>();
const [settings, setSettings] = useState<Settings>(); const [settings, setSettings] = useState<Settings>();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const result = await getBestSets({ term: value, offset: 0 }); setRefreshing(true);
const result = await getBestSets({ term: value, offset: 0 }).finally(() =>
setRefreshing(false)
);
setBests(result); setBests(result);
setOffset(0); setOffset(0);
}, []); }, []);
useFocusEffect( useEffect(() => {
useCallback(() => { refresh("");
refresh(term); settingsRepo.findOne({ where: {} }).then(setSettings);
}, [refresh, term]) const subs: EmitterSubscription[] = [];
);
subs.push(
DeviceEventEmitter.addListener(GYM_SET_CREATED, () => refresh("")),
DeviceEventEmitter.addListener(GYM_SET_UPDATED, () => refresh("")),
DeviceEventEmitter.addListener(GYM_SET_DELETED, () => refresh("")),
DeviceEventEmitter.addListener(SETTINGS, () =>
settingsRepo.findOne({ where: {} }).then(setSettings)
)
);
return () => subs.forEach((sub) => sub.remove());
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
const next = useCallback(async () => { const next = useCallback(async () => {
console.log("next:", { end, offset });
if (end) return; if (end) return;
const newOffset = offset + LIMIT; const newOffset = offset + LIMIT;
console.log(`${GraphsList.name}.next:`, { offset, newOffset, term }); console.log(`${GraphsList.name}.next:`, { offset, newOffset, term });
@ -68,7 +85,7 @@ export default function GraphsList() {
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`} description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
onPress={() => navigation.navigate("ViewGraph", { best: item })} onPress={() => navigation.navigate("ViewGraph", { best: item })}
left={() => left={() =>
(settings.images && item.image && ( (settings?.images && item.image && (
<Image <Image
source={{ uri: item.image }} source={{ uri: item.image }}
style={{ height: 75, width: 75 }} style={{ height: 75, width: 75 }}
@ -94,6 +111,8 @@ export default function GraphsList() {
renderItem={renderItem} renderItem={renderItem}
data={bests} data={bests}
onEndReached={next} onEndReached={next}
refreshing={refreshing}
onRefresh={() => refresh("")}
/> />
)} )}
</Page> </Page>

View File

@ -4,11 +4,20 @@ import {
useNavigation, useNavigation,
useRoute, useRoute,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { DeviceEventEmitter, FlatList } from "react-native"; import {
DeviceEventEmitter,
EmitterSubscription,
FlatList,
} from "react-native";
import { List } from "react-native-paper"; import { List } from "react-native-paper";
import { Like } from "typeorm"; import { Like } from "typeorm";
import { LIMIT } from "./constants"; import {
GYM_SET_CREATED,
GYM_SET_DELETED,
GYM_SET_UPDATED,
LIMIT,
} from "./constants";
import { getNow, setRepo, settingsRepo } from "./db"; import { getNow, setRepo, settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader"; import DrawerHeader from "./DrawerHeader";
import GymSet, { defaultSet } from "./gym-set"; import GymSet, { defaultSet } from "./gym-set";
@ -29,21 +38,14 @@ export default function SetList() {
const { params } = useRoute<RouteProp<HomePageParams, "Sets">>(); const { params } = useRoute<RouteProp<HomePageParams, "Sets">>();
const [term, setTerm] = useState(params?.search || ""); const [term, setTerm] = useState(params?.search || "");
const refresh = async ({ const refresh = async ({ value, take }: { value: string; take: number }) => {
value,
take,
skip,
}: {
value: string;
take: number;
skip: number;
}) => {
setRefreshing(true); setRefreshing(true);
setOffset(0);
const newSets = await setRepo const newSets = await setRepo
.find({ .find({
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any }, where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
take, take,
skip, skip: 0,
order: { created: "DESC" }, order: { created: "DESC" },
}) })
.finally(() => setRefreshing(false)); .finally(() => setRefreshing(false));
@ -57,20 +59,35 @@ export default function SetList() {
refresh({ refresh({
take: LIMIT, take: LIMIT,
value: "", value: "",
skip: 0,
}); });
const description = DeviceEventEmitter.addListener(SETTINGS, () => {
settingsRepo.findOne({ where: {} }).then(setSettings); const subs: EmitterSubscription[] = [];
});
return description.remove; subs.push(
DeviceEventEmitter.addListener(SETTINGS, () => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}),
DeviceEventEmitter.addListener(GYM_SET_UPDATED, () =>
refresh({ take: offset, value: term })
),
DeviceEventEmitter.addListener(GYM_SET_DELETED, () => {
refresh({ take: LIMIT, value: term });
}),
DeviceEventEmitter.addListener(GYM_SET_CREATED, () => {
refresh({ take: LIMIT, value: term });
}),
DeviceEventEmitter.addListener(SETTINGS, () =>
settingsRepo.findOne({ where: {} }).then(setSettings)
)
);
return () => subs.forEach((sub) => sub.remove());
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
}, []); }, []);
const search = (value: string) => { const search = (value: string) => {
setTerm(value); setTerm(value);
setOffset(0);
refresh({ refresh({
skip: 0,
take: LIMIT, take: LIMIT,
value, value,
}); });
@ -82,13 +99,11 @@ export default function SetList() {
if (params.search) search(params.search); if (params.search) search(params.search);
else if (params.refresh) else if (params.refresh)
refresh({ refresh({
skip: 0,
take: offset, take: offset,
value: term, value: term,
}); });
else if (params.reset) else if (params.reset)
refresh({ refresh({
skip: 0,
take: LIMIT, take: LIMIT,
value: term, value: term,
}); });
@ -164,8 +179,8 @@ export default function SetList() {
const remove = async () => { const remove = async () => {
setIds([]); setIds([]);
await setRepo.delete(ids.length > 0 ? ids : {}); await setRepo.delete(ids.length > 0 ? ids : {});
DeviceEventEmitter.emit(GYM_SET_DELETED);
return refresh({ return refresh({
skip: 0,
take: LIMIT, take: LIMIT,
value: term, value: term,
}); });
@ -194,9 +209,7 @@ export default function SetList() {
onEndReached={next} onEndReached={next}
refreshing={refreshing} refreshing={refreshing}
onRefresh={() => { onRefresh={() => {
setOffset(0);
refresh({ refresh({
skip: 0,
take: LIMIT, take: LIMIT,
value: term, value: term,
}); });

View File

@ -6,11 +6,17 @@ import {
useRoute, useRoute,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { FlatList, NativeModules, TextInput, View } from "react-native"; import {
DeviceEventEmitter,
FlatList,
NativeModules,
TextInput,
View,
} from "react-native";
import { Button, IconButton, ProgressBar } from "react-native-paper"; import { Button, IconButton, ProgressBar } from "react-native-paper";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import { getBestSet } from "./best.service"; import { getBestSet } from "./best.service";
import { MARGIN, PADDING } from "./constants"; import { GYM_SETS_EDITED, GYM_SET_CREATED, MARGIN, PADDING } from "./constants";
import CountMany from "./count-many"; import CountMany from "./count-many";
import { AppDataSource } from "./data-source"; import { AppDataSource } from "./data-source";
import { getNow, setRepo, settingsRepo } from "./db"; import { getNow, setRepo, settingsRepo } from "./db";
@ -103,6 +109,7 @@ export default function StartPlan() {
hidden: false, hidden: false,
}; };
await setRepo.save(newSet); await setRepo.save(newSet);
DeviceEventEmitter.emit(GYM_SET_CREATED);
await refresh(); await refresh();
if ( if (
settings.notify && settings.notify &&

View File

@ -1,8 +1,14 @@
import { NavigationProp, useNavigation } from "@react-navigation/native"; import { NavigationProp, useNavigation } from "@react-navigation/native";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { GestureResponderEvent, ListRenderItemInfo, View } from "react-native"; import {
DeviceEventEmitter,
GestureResponderEvent,
ListRenderItemInfo,
View,
} from "react-native";
import { List, Menu, RadioButton, useTheme } from "react-native-paper"; import { List, Menu, RadioButton, useTheme } from "react-native-paper";
import { Like } from "typeorm"; import { Like } from "typeorm";
import { GYM_SET_DELETED } from "./constants";
import CountMany from "./count-many"; import CountMany from "./count-many";
import { getNow, setRepo } from "./db"; import { getNow, setRepo } from "./db";
import { HomePageParams } from "./home-page-params"; import { HomePageParams } from "./home-page-params";
@ -38,6 +44,7 @@ export default function StartPlanItem(props: Props) {
setShowMenu(false); setShowMenu(false);
if (!first) return toast("Nothing to undo."); if (!first) return toast("Nothing to undo.");
await setRepo.delete(first.id); await setRepo.delete(first.id);
DeviceEventEmitter.emit(GYM_SET_DELETED);
onUndo(); onUndo();
}, [setShowMenu, onUndo, item.name]); }, [setShowMenu, onUndo, item.name]);

View File

@ -4,3 +4,6 @@ export const ITEM_PADDING = 8;
export const DARK_RIPPLE = "#444444"; export const DARK_RIPPLE = "#444444";
export const LIGHT_RIPPLE = "#c2c2c2"; export const LIGHT_RIPPLE = "#c2c2c2";
export const LIMIT = 15; export const LIMIT = 15;
export const GYM_SET_UPDATED = "gym-set-updated";
export const GYM_SET_CREATED = "gym-set-updated";
export const GYM_SET_DELETED = "gym-set-updated";

View File

@ -1,5 +1,6 @@
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
import { GymSetSubscriber } from "./gym-set-subscriber";
import { Sets1667185586014 as sets1667185586014 } from "./migrations/1667185586014-sets"; import { Sets1667185586014 as sets1667185586014 } from "./migrations/1667185586014-sets";
import { plans1667186124792 } from "./migrations/1667186124792-plans"; import { plans1667186124792 } from "./migrations/1667186124792-plans";
import { settings1667186130041 } from "./migrations/1667186130041-settings"; import { settings1667186130041 } from "./migrations/1667186130041-settings";

View File

@ -3,16 +3,6 @@ import GymSet from "./gym-set";
export type HomePageParams = { export type HomePageParams = {
Sets: { Sets: {
search?: string; search?: string;
/**
* Reload the current list with limit = offset
*/
refresh?: number;
/**
* Reload the list with limit = 0
*/
reset?: number;
}; };
EditSet: { EditSet: {
set: GymSet; set: GymSet;