Skip to content

Commit f79d3f3

Browse files
author
Manuel Ruck
committed
Refactor procedures list handling and improve error/retry states with new components
1 parent 76754a4 commit f79d3f3

File tree

5 files changed

+408
-197
lines changed

5 files changed

+408
-197
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { Text } from "react-native";
3+
4+
import { Button } from "../../../../components/Button";
5+
import { Centered } from "../../../../components/Centered";
6+
7+
interface ErrorStateProps {
8+
onRetry: () => void;
9+
}
10+
11+
export const ErrorState: React.FC<ErrorStateProps> = ({ onRetry }) => (
12+
<Centered>
13+
<Text>Verbindungsfehler</Text>
14+
<Button
15+
onPress={onRetry}
16+
text="Nochmal versuchen"
17+
textColor="blue"
18+
backgroundColor="transparent"
19+
/>
20+
</Centered>
21+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import styled from "styled-components/native";
3+
4+
import { Centered } from "../../../../components/Centered";
5+
import { ListLoading } from "../../../../components/ListLoading";
6+
7+
interface RetryStateProps {
8+
remainingAttempts: number;
9+
}
10+
11+
export const RetryState: React.FC<RetryStateProps> = ({
12+
remainingAttempts,
13+
}) => (
14+
<Centered>
15+
<ListLoading />
16+
<RetryMessage>Verbindung wird wiederhergestellt…</RetryMessage>
17+
<RetryHint>
18+
Automatischer Versuch in Kürze. Verbleibende Versuche: {remainingAttempts}
19+
</RetryHint>
20+
</Centered>
21+
);
22+
23+
const RetryMessage = styled.Text`
24+
margin-top: 16px;
25+
text-align: center;
26+
color: ${({ theme }) => theme.colors.text.primary};
27+
`;
28+
29+
const RetryHint = styled.Text`
30+
margin-top: 4px;
31+
text-align: center;
32+
color: ${({ theme }) => theme.colors.text.secondary};
33+
font-size: 12px;
34+
`;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2+
import { ApolloError, ApolloQueryResult } from "@apollo/client";
3+
import { useRecoilValue } from "recoil";
4+
5+
import { useListFilter } from "../../../../api/hooks/useListFilter";
6+
import { constituencyState } from "../../../../api/state/constituency";
7+
import { useLegislaturePeriodStore } from "../../../../api/state/legislaturePeriod";
8+
import {
9+
ParlamentIdentifier,
10+
parlaments,
11+
} from "../../../../api/state/parlament";
12+
import {
13+
ListType,
14+
ProceduresListQuery,
15+
useProceduresListQuery,
16+
} from "../../../../__generated__/graphql";
17+
18+
export interface SegmentedData {
19+
title: string;
20+
data: ProceduresListQuery["procedures"];
21+
}
22+
23+
interface UseProceduresListResult {
24+
procedures: ProceduresListQuery["procedures"];
25+
segmentedData: SegmentedData[];
26+
loading: boolean;
27+
isRetrying: boolean;
28+
remainingAttempts: number;
29+
error: ApolloError | undefined;
30+
hasMore: boolean;
31+
networkStatus?: number;
32+
handleManualRefetch: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
33+
handleRefresh: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
34+
handleEndReached: () => Promise<void>;
35+
}
36+
37+
const MAX_RETRY_ATTEMPTS = 3;
38+
const BASE_RETRY_DELAY_MS = 1500;
39+
40+
export const useProceduresList = (list: ListType): UseProceduresListResult => {
41+
const { proceduresFilter } = useListFilter();
42+
const constituency = useRecoilValue(constituencyState);
43+
const { legislaturePeriod } = useLegislaturePeriodStore();
44+
const parlamentIdentifier = `BT-${legislaturePeriod}` as ParlamentIdentifier;
45+
const parlament = parlaments[parlamentIdentifier];
46+
47+
const constituencies = useMemo(
48+
() => (constituency ? [constituency] : []),
49+
[constituency]
50+
);
51+
52+
const queryVariables = useMemo(
53+
() => ({
54+
listTypes: [list],
55+
pageSize: 10,
56+
filter: proceduresFilter,
57+
constituencies,
58+
period: parlament?.period,
59+
}),
60+
[constituencies, list, parlament?.period, proceduresFilter]
61+
);
62+
63+
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
64+
const [retryAttempt, setRetryAttempt] = useState(0);
65+
const [isRetryScheduled, setIsRetryScheduled] = useState(false);
66+
const [hasMore, setHasMore] = useState(true);
67+
68+
const clearRetryTimeout = useCallback(() => {
69+
if (retryTimeoutRef.current) {
70+
clearTimeout(retryTimeoutRef.current);
71+
retryTimeoutRef.current = null;
72+
}
73+
}, []);
74+
75+
const { loading, data, error, fetchMore, networkStatus, refetch } =
76+
useProceduresListQuery({
77+
fetchPolicy: "network-only",
78+
errorPolicy: "all",
79+
variables: queryVariables,
80+
});
81+
82+
const scheduleRetry = useCallback(() => {
83+
setIsRetryScheduled((prev) => {
84+
if (prev || retryAttempt >= MAX_RETRY_ATTEMPTS) {
85+
return prev;
86+
}
87+
clearRetryTimeout();
88+
const delay = BASE_RETRY_DELAY_MS * (retryAttempt + 1);
89+
retryTimeoutRef.current = setTimeout(() => {
90+
setIsRetryScheduled(false);
91+
setRetryAttempt((attempt) => attempt + 1);
92+
void refetch(queryVariables);
93+
}, delay);
94+
return true;
95+
});
96+
}, [clearRetryTimeout, queryVariables, refetch, retryAttempt]);
97+
98+
useEffect(() => {
99+
if (!loading && (error || !data)) {
100+
scheduleRetry();
101+
}
102+
}, [data, error, loading, scheduleRetry]);
103+
104+
useEffect(() => {
105+
if (data) {
106+
if (retryAttempt !== 0) {
107+
setRetryAttempt(0);
108+
}
109+
if (isRetryScheduled) {
110+
setIsRetryScheduled(false);
111+
}
112+
clearRetryTimeout();
113+
}
114+
}, [clearRetryTimeout, data, isRetryScheduled, retryAttempt]);
115+
116+
useEffect(() => () => clearRetryTimeout(), [clearRetryTimeout]);
117+
118+
useEffect(() => {
119+
setHasMore(true);
120+
setRetryAttempt(0);
121+
setIsRetryScheduled(false);
122+
clearRetryTimeout();
123+
}, [
124+
clearRetryTimeout,
125+
constituencies,
126+
list,
127+
parlament?.period,
128+
proceduresFilter,
129+
]);
130+
131+
const handleManualRefetch = useCallback(() => {
132+
setRetryAttempt(0);
133+
setIsRetryScheduled(false);
134+
clearRetryTimeout();
135+
return refetch(queryVariables);
136+
}, [clearRetryTimeout, queryVariables, refetch]);
137+
138+
const handleRefresh = useCallback(() => {
139+
setHasMore(true);
140+
return handleManualRefetch();
141+
}, [handleManualRefetch]);
142+
143+
const handleEndReached = useCallback(() => {
144+
if (loading || !hasMore || !data) {
145+
return Promise.resolve();
146+
}
147+
148+
return fetchMore({
149+
variables: {
150+
offset: data.procedures.length,
151+
},
152+
}).then(({ data: fetchMoreData }) => {
153+
if (!fetchMoreData || fetchMoreData.procedures.length === 0) {
154+
setHasMore(false);
155+
}
156+
});
157+
}, [data, fetchMore, hasMore, loading]);
158+
159+
const segmentedData: SegmentedData[] = useMemo(() => {
160+
if (data && ListType.Top100 === list) {
161+
return [
162+
{
163+
title: "",
164+
data: data.procedures,
165+
},
166+
];
167+
}
168+
169+
if (!data) {
170+
return [];
171+
}
172+
173+
return data.procedures.reduce<SegmentedData[]>((prev, procedure) => {
174+
const { voteWeek, voteYear } = procedure;
175+
const segment = voteWeek && voteYear ? `KW ${voteWeek}/${voteYear}` : "";
176+
const index = prev.findIndex(({ title }) => title === segment);
177+
178+
if (index !== -1) {
179+
return Object.assign([...prev], {
180+
[index]: { title: segment, data: [...prev[index].data, procedure] },
181+
});
182+
}
183+
184+
return [
185+
...prev,
186+
{
187+
title: segment,
188+
data: [procedure],
189+
},
190+
];
191+
}, []);
192+
}, [data, list]);
193+
194+
const isRetrying =
195+
!loading &&
196+
(error || !data) &&
197+
(isRetryScheduled || retryAttempt > 0) &&
198+
retryAttempt < MAX_RETRY_ATTEMPTS;
199+
200+
const remainingAttempts = Math.max(0, MAX_RETRY_ATTEMPTS - retryAttempt);
201+
const procedures = data?.procedures ?? [];
202+
203+
return {
204+
procedures,
205+
segmentedData,
206+
loading,
207+
isRetrying,
208+
remainingAttempts,
209+
error,
210+
hasMore,
211+
networkStatus,
212+
handleManualRefetch,
213+
handleRefresh,
214+
handleEndReached,
215+
};
216+
};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useCallback } from "react";
2+
import { ListRenderItem } from "react-native";
3+
import { useRouter } from "expo-router";
4+
5+
import { useLocalVotes } from "../../../../api/state/localVotesStore";
6+
import { communityVoteData } from "../../../../lib/PieChartCommunityData";
7+
import { pieChartGovernmentData } from "../../../../lib/PieChartGovernmentData";
8+
import { Row } from "../../../../components/Row";
9+
import { ListItem } from "../../../../components/ListItem";
10+
import {
11+
ListType,
12+
ProceduresListQuery,
13+
} from "../../../../__generated__/graphql";
14+
15+
export type ProcedureListItem = ProceduresListQuery["procedures"][0];
16+
17+
export const useProceduresListItemRenderer = (
18+
list: ListType
19+
): ListRenderItem<ProcedureListItem> => {
20+
const router = useRouter();
21+
const localVotes = useLocalVotes();
22+
23+
const handleProcedurePress = useCallback(
24+
(procedureId: string) => {
25+
router.push(`/procedure/${procedureId}`);
26+
},
27+
[router]
28+
);
29+
30+
return useCallback<ListRenderItem<ProcedureListItem>>(
31+
({
32+
item: {
33+
procedureId,
34+
title,
35+
sessionTOPHeading,
36+
subjectGroups,
37+
voteDate,
38+
voteEnd,
39+
voted: votedServer,
40+
voteResults,
41+
votedGovernment,
42+
communityVotes,
43+
},
44+
index,
45+
}) => {
46+
let subline: string | null = null;
47+
if (sessionTOPHeading) {
48+
subline = sessionTOPHeading;
49+
} else if (subjectGroups) {
50+
subline = subjectGroups.join(", ");
51+
}
52+
53+
const govSlices = pieChartGovernmentData({
54+
voteResults,
55+
votedGovernment,
56+
});
57+
58+
const localSelection = localVotes.find(
59+
(localVote) => localVote.procedureId === procedureId
60+
)?.selection;
61+
const voted = votedServer || !!localSelection;
62+
63+
const communityVoteSlices = communityVoteData({
64+
communityVotes,
65+
localSelection,
66+
voted,
67+
});
68+
69+
return (
70+
<Row
71+
onPress={() => handleProcedurePress(procedureId)}
72+
testID={`ListItem-${list}-${index}`}
73+
>
74+
<ListItem
75+
title={title}
76+
subline={subline}
77+
voteDate={voteDate ? new Date(voteDate) : undefined}
78+
endDate={voteEnd ? new Date(voteEnd) : undefined}
79+
voted={voted}
80+
votes={communityVotes ? communityVotes.total || 0 : 0}
81+
govermentChart={{
82+
votes: govSlices,
83+
large: true,
84+
}}
85+
communityVotes={communityVoteSlices}
86+
/>
87+
</Row>
88+
);
89+
},
90+
[handleProcedurePress, list, localVotes]
91+
);
92+
};

0 commit comments

Comments
 (0)