Skip to content

Commit

Permalink
Resolves a merge conflict.
Browse files Browse the repository at this point in the history
Rebase. This commit includes the following commits:

feat(various): Created a hook out of the NetworkDetector component

feat(various): Refactored the NetworkDetector component so that it shows a barrier if the user goes offline; Moved the use of the useNetworkDetection hook to the NetworkDetector component

feat(notificationApi): Made the notificationApi file consistent with the other api files when it comes to refetching data

feat(store): Data is now being refetched when the user's connection is restored

feat(useReducerInfiniteLoading): In the middle of creating a version of useInfiniteLoading that uses the reducer from useLiveNotifications

feat(various): Added the useNewLiveNotifications hook for testing
purposes and resolves some issues

feat(useReducerInfiniteLoading): The user's unread notifications are now cleared before refetching in the internet reconnection scenario.

fix(useReducerInfiniteLoading): Resolved the issue that was causing the
notifications to be cleared when more notifications are fetched

fix(various): Removed the clearing that was happening when the
notification dropdown was closed. This is already handled by the
useReducerInfiniteLoading hook.

refactor(various): Renamed notification to item in the
useReducerInfiniteLoading hook

refactor(useReducerInfiniteLoading): Implemented a better solution for the duplicate item issue when the user's internet connection is restored

refactor(useReducerInfiniteLoading): Removed some unnecessary code and
added some console logs in order to help identify why this hook keeps
executing

fix(useReducerInfiniteLoading): Resolves the infinite re-rendering of the notification listview

fix(useReducerInfiniteLoading): Corrected the useReducerInfiniteLoading
hook so that it returns the right type for the items and made it be
compatible with the pages that use the useInfiniteLoading hook

refactor(various): Now using the useReducerInfiniteLoading hook in all
of the places where the useInfiniteLoading hook was being used

fix(useReducerInfiniteLoading): Forget to add the error value to the
useMemo dependency list

fix(useReducerInfiniteLoading): The nextItemUrl wasn't being set correctly when the reducer state was reset.

fix(various): I found that the resetApiState call would cause the
infinite loading functionality to just refresh the whole page instead of
working as you would aspect. Removing it fixed this issue. Based on my
testing, I found it wasn't necessary for the notification functionality.

fix(various): It was incorrect to remove the resetApiState function call
for the notification functionality.

refactor(various): Removed the old versions of useLiveNotifications and useInfiniteLoading and replaced them with the new ones

refactored(useLiveNotifications): Removed some commented out code

refactor(NetworkDetector): Removed the InteractionBarrier since the service worker PR will make this functionality unnecessary

refactor(various): Renamed WithNumberIdentifier to WithIdentifier and made its id property accept string values as well

refactor(various): In the middle of making the infinite loading
functionality simpler

refactor(various): Mostly everything is working. Just need to get the
count on the NotificationListView to update when a notification is
removed.

feat(various): The count on the NotificationListView is now being
updated correctly.

refactor(various): Renamed the addOne and addMultiple cases

refactor(useInfinteLoading): Need to implement a better solution for
updating the count on NotificationListView

refactor(various): The count is now being updated correctly again.

refactor(various): Removed unnecessary properties and console logs; Resolved eslint issues
  • Loading branch information
joshwingreene committed May 17, 2023
1 parent 4718d15 commit 49c1d9e
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 196 deletions.
3 changes: 3 additions & 0 deletions src/app/redux/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { configureStore, ConfigureStoreOptions } from '@reduxjs/toolkit';
import { farmApi } from 'common/api/farmApi';
import { setupListeners } from '@reduxjs/toolkit/query';
import { authApi } from 'common/api/authApi';
import { notificationApi } from 'common/api/notificationApi';
import { userApi } from 'common/api/userApi';
Expand Down Expand Up @@ -31,6 +32,8 @@ export const createAppStore = (options?: ConfigureStoreOptions['preloadedState']

export const store = createAppStore();

setupListeners(store.dispatch);

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;
7 changes: 7 additions & 0 deletions src/common/api/notificationApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { customBaseQuery } from './customBaseQuery';

export const notificationApi = createApi({
reducerPath: 'notificationApi',

baseQuery: customBaseQuery,

// Always refetch data, don't used cache.
keepUnusedDataFor: 0,
refetchOnMountOrArgChange: true,
refetchOnReconnect: true,

tagTypes: ['AppNotification'],

endpoints: builder => ({
Expand Down
159 changes: 118 additions & 41 deletions src/common/hooks/useInfiniteLoading.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,135 @@
import { ActionCreatorWithoutPayload } from '@reduxjs/toolkit';
import { PaginatedResult } from 'common/models';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useDispatch } from 'react-redux';
import { UseQuery, UseQueryOptions } from 'rtk-query-config';

export const useInfiniteLoading = <T, ResultType extends PaginatedResult<T>>(
initialUrl: string,
export interface WithIdentifier {
id?: number | string;
}

interface State<T> {
items: T[];
nextItemUrl: string | null;
count: number;
isGettingMore: boolean;
}

const initialState = {
items: [],
nextItemUrl: null,
count: 0,
isGettingMore: false,
};

type Action<T> =
| { type: 'addOneToFront'; item: T }
| { type: 'addMultipleToBack'; items: T[]; totalCount: number }
| { type: 'set-next-item-url'; nextItemUrl: string | null }
| { type: 'reset-get-more' }
| { type: 'remove'; item: T }
| { type: 'reset'; nextItemUrl: string | null };

const reducer = <T extends WithIdentifier>(state: State<T>, action: Action<T>) => {
switch (action.type) {
case 'addOneToFront':
return { ...state, items: [action.item, ...state.items], count: state.count + 1 };
case 'addMultipleToBack':
return { ...state, items: [...state.items, ...action.items], count: action.totalCount };
case 'remove':
return {
...state,
items: state.items.filter(i => i.id !== action.item.id),
count: state.count - 1,
};
case 'reset':
return { ...initialState, nextItemUrl: action.nextItemUrl };
case 'set-next-item-url':
return { ...state, nextItemUrl: action.nextItemUrl, isGettingMore: true, allItemsRemoved: false };
default:
return { ...initialState };
}
};

export const useInfiniteLoading = <T extends WithIdentifier, ResultType extends PaginatedResult<T>>(
initialUrl: string | null,
useQuery: UseQuery<ResultType>,
resetApiStateFunction?: ActionCreatorWithoutPayload,
options?: UseQueryOptions,
) => {
const [url, setUrl] = useState<string | null>(initialUrl);
const [loadedData, setLoadedData] = useState<T[]>([]);
const rerenderingType = useRef<string>('clear');
const [{ items, nextItemUrl, count, isGettingMore }, itemDispatch] = useReducer(reducer, {
...initialState,
nextItemUrl: initialUrl,
});
const dispatch = useDispatch();

const { data, error, isLoading, isFetching } = useQuery(url, options);
const { data: fetchedItems, isFetching, isLoading, refetch, error } = useQuery(nextItemUrl, options);

useEffect(() => {
const clear = () => {
rerenderingType.current = 'clear';
setLoadedData([]);
setUrl(initialUrl);
};
const addOneToFront = useCallback(
(newItem: T) => {
itemDispatch({ type: 'addOneToFront', item: newItem });
},
[itemDispatch],
);

if (data && !isLoading) {
setLoadedData(n => [...n, ...data.results]);
const clear = useCallback(() => {
itemDispatch({ type: 'reset', nextItemUrl: initialUrl });
if (resetApiStateFunction) {
dispatch(resetApiStateFunction());
}
}, [itemDispatch, initialUrl, dispatch, resetApiStateFunction]);

return () => {
if (rerenderingType.current === 'clear') {
clear();
}
if (rerenderingType.current === 'fetchMore') {
rerenderingType.current = 'clear';
}
};
}, [data, isLoading, initialUrl]);
const remove = useCallback(
(itemToRemove: T) => {
itemDispatch({ type: 'remove', item: itemToRemove });
},
[itemDispatch],
);

const hasMore = useMemo(() => {
if (isLoading || isFetching) return false;
return !!data?.links.next;
}, [data, isLoading, isFetching]);
return !!fetchedItems?.links.next;
}, [fetchedItems, isLoading, isFetching]);

const fetchMore = () => {
if (hasMore && data) {
rerenderingType.current = 'fetchMore';
setUrl(data.links.next);
const getMore = useCallback(() => {
if (fetchedItems?.links.next && !isFetching) {
itemDispatch({ type: 'set-next-item-url', nextItemUrl: fetchedItems.links.next });
}
};

return {
loadedData,
error,
isLoading,
isFetching,
totalCount: data?.meta.count,
hasMore,
fetchMore,
};
}, [itemDispatch, isFetching, fetchedItems]);

// Clear the items when the user's internet connection is restored
useEffect(() => {
if (!isLoading && isFetching && !isGettingMore) {
clear();
}
}, [isLoading, isFetching, isGettingMore, clear]);

// Append new items that we got from the API to
// the items list
useEffect(() => {
itemDispatch({
type: 'addMultipleToBack',
items: fetchedItems?.results || [],
totalCount: fetchedItems?.meta.count || 0,
});
}, [fetchedItems]);

const itemProviderValue = useMemo(() => {
const result = {
items: items as T[],
count,
hasMore,
isFetching,
isLoading,
remove,
clear,
getMore,
refetch,
addOneToFront,
error,
};
return result;
}, [clear, remove, getMore, hasMore, items, count, isFetching, isLoading, addOneToFront, refetch, error]);

return itemProviderValue;
};
19 changes: 11 additions & 8 deletions src/features/farm-dashboard/pages/UpdateFarmView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
import { FarmDetailForm, FormData } from '../components/FarmDetailForm';
import { useAuth } from 'features/auth/hooks';
import { ChangeLog } from 'common/components/ChangeLog/ChangeLog';
import { useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
import { QueryParamsBuilder } from 'common/api/queryParamsBuilder';
import { HistoricalRecord } from 'common/models/historicalRecord';
import { ChangeListGroup } from 'common/components/ChangeLog/ChangeListGroup';
import { LoadingButton } from 'common/components/LoadingButton';
import { useModal } from 'react-modal-hook';
import { DimmableContent } from 'common/styles/utilities';
import { WithIdentifier, useInfiniteLoading } from 'common/hooks/useInfiniteLoading';

export type RouteParams = {
id: string;
Expand All @@ -35,15 +35,18 @@ export const UpdateFarmView: FC = () => {
const queryParams = new QueryParamsBuilder().setPaginationParams(1, pageSize).build();
const url = `/farms/${id}/history/?${queryParams}`;
const {
loadedData: farmHistory,
items: farmHistory,
error: farmHistoryError,
isFetching: isFetchingHistory,
totalCount,
count: totalCount,
hasMore,
fetchMore,
} = useInfiniteLoading<HistoricalRecord<User>, PaginatedResult<HistoricalRecord<User>>>(url, useGetFarmHistoryQuery, {
skip: user?.role !== 'ADMIN',
});
getMore,
} = useInfiniteLoading<HistoricalRecord<User> & WithIdentifier, PaginatedResult<HistoricalRecord<User>>>(
url,
useGetFarmHistoryQuery,
undefined,
{ skip: user?.role !== 'ADMIN' },
);

const [formValidationErrors, setFormValidationErrors] = useState<ServerValidationErrors<FormData> | null>(null);

Expand All @@ -65,7 +68,7 @@ export const UpdateFarmView: FC = () => {
className='action-shadow'
loading={isFetchingHistory}
variant='primary'
onClick={() => fetchMore()}
onClick={() => getMore()}
>
Load More
</LoadingButton>
Expand Down
33 changes: 3 additions & 30 deletions src/features/network-detector/components/NetworkDetector.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,8 @@
import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react';
import * as notificationService from 'common/services/notification';
import { FC, PropsWithChildren } from 'react';
import { useNetworkDetection } from '../hooks/useNetworkConnection';

export const NetworkDetector: FC<PropsWithChildren<unknown>> = ({ children }) => {
const [isDisconnected, setDisconnectedStatus] = useState(false);
const prevDisconnectionStatus = useRef(false);

const handleConnectionChange = () => {
setDisconnectedStatus(!navigator.onLine);
};

const getRandomNumber = () => {
return new Date().valueOf().toString();
};

useEffect(() => {
window.addEventListener('online', handleConnectionChange);
window.addEventListener('offline', handleConnectionChange);

if (isDisconnected) {
notificationService.showErrorMessage('Internet Connection Lost', getRandomNumber());
} else if (prevDisconnectionStatus.current) {
notificationService.showSuccessMessage('Internet Connection Restored', getRandomNumber());
}

prevDisconnectionStatus.current = isDisconnected;

return () => {
window.removeEventListener('online', handleConnectionChange);
window.removeEventListener('offline', handleConnectionChange);
};
}, [isDisconnected]);
useNetworkDetection();

return <>{children}</>;
};
35 changes: 35 additions & 0 deletions src/features/network-detector/hooks/useNetworkConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState, useRef, useEffect } from 'react';
import * as notificationService from 'common/services/notification';

export const getRandomNumber = () => {
return new Date().valueOf().toString();
};

export const useNetworkDetection = () => {
const [isDisconnected, setDisconnectedStatus] = useState(false);
const prevDisconnectionStatus = useRef(false);

const handleConnectionChange = () => {
setDisconnectedStatus(!navigator.onLine);
};

useEffect(() => {
window.addEventListener('online', handleConnectionChange);
window.addEventListener('offline', handleConnectionChange);

if (isDisconnected) {
notificationService.showErrorMessage('Internet Connection Lost', getRandomNumber());
} else if (prevDisconnectionStatus.current) {
notificationService.showSuccessMessage('Internet Connection Restored', getRandomNumber());
}

prevDisconnectionStatus.current = isDisconnected;

return () => {
window.removeEventListener('online', handleConnectionChange);
window.removeEventListener('offline', handleConnectionChange);
};
}, [isDisconnected]);

return { isDisconnected };
};
16 changes: 11 additions & 5 deletions src/features/notifications/components/NotificationDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { faBell, faEnvelope, faEnvelopeOpen } from '@fortawesome/free-solid-svg-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useGetReadNotificationsQuery, useMarkAllReadMutation } from 'common/api/notificationApi';
import { LoadingButton } from 'common/components/LoadingButton';
import { useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
import { WithIdentifier, useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
import { PaginatedResult } from 'common/models';
import { AppNotification } from 'common/models/notifications';
import { NoContent } from 'common/styles/utilities';
Expand All @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import { NotificationContext } from '../context';
import { renderNotification } from './renderNotification';
import { notificationApi } from 'common/api/notificationApi';

const StyledContainer = styled.div`
min-width: 420px;
Expand Down Expand Up @@ -72,10 +73,15 @@ export const NotificationDropdown: FC = () => {
count: unreadNotificationsCount,
clear: clearUnreadNotifications,
} = useContext(NotificationContext);
const { loadedData: readNotifications, isLoading: isLoadingReadNotifications } = useInfiniteLoading<
AppNotification,
PaginatedResult<AppNotification>
>('', useGetReadNotificationsQuery);
const {
items: readNotifications,
isLoading: isLoadingReadNotifications,
hasMore: hasMoreReadNotifications,
} = useInfiniteLoading<AppNotification & WithIdentifier, PaginatedResult<AppNotification>>(
'',
useGetReadNotificationsQuery,
notificationApi.util.resetApiState,
);
const [markAllRead, { isLoading: isLoadingMarkAllRead }] = useMarkAllReadMutation();

const handleMarkAllRead = async () => {
Expand Down
Loading

0 comments on commit 49c1d9e

Please sign in to comment.