Skip to content

Commit

Permalink
fix: Clean up useStorageItem logic.
Browse files Browse the repository at this point in the history
  • Loading branch information
darkobits committed Dec 18, 2021
1 parent 1147722 commit 199f8f3
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 34 deletions.
16 changes: 10 additions & 6 deletions packages/client/src/components/AnimatedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ interface Props extends ElementProps<HTMLModElement> {
footer?: React.ReactNode;
}

export const AnimatedModal = (props: Props) => {
export const AnimatedModal = React.memo((props: Props) => {
const { id, show, body, footer, onBeginHide, onClose, className } = props;
const [showInternal, setShowInternal] = React.useState(show ?? false);
const [showInternal, setShowInternal] = React.useState(false);


/**
Expand All @@ -31,7 +31,8 @@ export const AnimatedModal = (props: Props) => {
setShowInternal(false);
if (onClose) onClose();
}
}, [onBeginHide, onClose]);
}, [onClose]);


/**
* [Effect] When `show` becomes truthy, immediately show the modal. When it
Expand All @@ -40,14 +41,17 @@ export const AnimatedModal = (props: Props) => {
* finishes.
*/
React.useEffect(() => {
if (show) {
if (show === true) {
setShowInternal(true);
return;
}

void handleClose();
if (show === false) {
void handleClose();
}
}, [show, handleClose]);


return (
<Modal
id={id}
Expand All @@ -66,4 +70,4 @@ export const AnimatedModal = (props: Props) => {
</Modal.Body>
</Modal>
);
};
});
14 changes: 8 additions & 6 deletions packages/client/src/components/Greeting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as R from 'ramda';
import React from 'react';

import { useInspirat } from 'hooks/use-inspirat';
import { isPending } from 'hooks/use-storage-item';
import { getPeriodDescriptor } from 'lib/time';
import { compositeTextShadow } from 'lib/typography';
import { rgba } from 'lib/utils';
Expand Down Expand Up @@ -43,9 +44,9 @@ const GreetingWrapper = styled.div<StyledGreetingProps>`
opacity: ${R.prop('opacity')};
transition-property: opacity;
transition-property: all;
transition-duration: 1.2s;
transition-timing-function: ease-in;
transition-timing-function: ease-in-out;
transition-delay: 0s;
* {
Expand Down Expand Up @@ -87,6 +88,7 @@ interface GreetingProps {
palette: InspiratPhotoResource['palette'] | undefined;
}


const GreetingForeground = styled.div<GreetingProps>`
align-items: center;
color: inherit;
Expand All @@ -100,6 +102,7 @@ const GreetingForeground = styled.div<GreetingProps>`
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.8);
`;


const GreetingBackground = styled.div<GreetingProps>`
align-items: center;
color: ${props => {
Expand All @@ -119,8 +122,6 @@ const GreetingBackground = styled.div<GreetingProps>`
`;


// ----- Greeting --------------------------------------------------------------

/**
* Renders the greeting copy.
*/
Expand All @@ -140,8 +141,9 @@ const Greeting: React.FunctionComponent = () => {
return () => clearInterval(interval);
}, []);


const greeting = name ? `Good ${period}, ${name}.` : `Good ${period}.`;
const greeting = isPending(name)
? `Good ${period}.`
: `Good ${period}, ${name}.`;
const color = rgba(currentPhoto?.palette?.vibrant ?? {r: 0, g: 0, b: 0});

return (
Expand Down
13 changes: 8 additions & 5 deletions packages/client/src/components/Introduction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from 'react-bootstrap';

import { AnimatedModal } from 'components/AnimatedModal';
import { useInspirat } from 'hooks/use-inspirat';
import { isPending } from 'hooks/use-storage-item';
import { isChromeExtension } from 'lib/utils';


Expand All @@ -25,9 +26,12 @@ const styles = {
export const Introduction: React.FunctionComponent = () => {
const { hasSeenIntroduction, setHasSeenIntroduction } = useInspirat();


/**
* [Event Handler] When the modal backdrop is clicked or the 'OK' button is
* pressed, update the introduction flag, which will hide the modal.
*/
const handleClose = React.useCallback(() => {
setHasSeenIntroduction(true);
if (setHasSeenIntroduction) setHasSeenIntroduction(true);
}, [setHasSeenIntroduction]);


Expand All @@ -43,9 +47,8 @@ export const Introduction: React.FunctionComponent = () => {
), []);


if (!isChromeExtension()) {
return null;
}
if (!isChromeExtension() || isPending(hasSeenIntroduction)) return null;


return (
<AnimatedModal
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const Settings: React.FunctionComponent<SettingsProps> = ({ show, onClose
* [Effect] Replicates global state to local state.
*/
React.useEffect(() => {
setTempName(name);
if (typeof name === 'string') setTempName(name);
}, [name, setTempName, show]);


Expand Down
5 changes: 1 addition & 4 deletions packages/client/src/components/Splash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const SplashEl = styled.div`
justify-content: center;
padding: 14px 18px;
width: 100%;
transition: opacity 0.4s ease-in;
transition: opacity 1.2s ease-in-out;
`;


Expand All @@ -50,9 +50,6 @@ export const Splash: React.FunctionComponent<SplashProps> = ({ onMouseDown }) =>
const { currentPhoto, currentPhotoUrls } = useInspirat();
const [activeElement, toggleActiveElement] = React.useReducer((prev: string) => (prev === 'A' ? 'B' : 'A'), 'A');

// This is used to delay showing the greeting until the photo has loaded.
// const opacity = currentPhoto ? 1 : 0;


/**
* [Effect] When the current photo changes, updates the background-image URL
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/components/SplashLower.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const StyledSplashLower = styled.div<StyledSplashLowerProps>`
justify-content: space-between;
opacity: ${R.prop('opacity')};
transition: opacity 1.2s ease-in;
transition-delay: 1.2s;
/* Force the element to the flex-end of its parent. */
margin-top: auto;
width: 100%;
z-index: 1;
`;
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/hooks/use-inspirat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export interface InspiratHook {
const preloadedPhotos = new Set<string>();


// @ts-expect-error
export const useInspirat = singletonHook({} as InspiratHook, () => {
const [currentPhotoUrls, setCurrentPhotoUrls] = React.useState<PhotoUrls>();
const [currentPhotoFromState, setCurrentPhoto] = React.useState<InspiratPhotoResource>();
Expand All @@ -114,7 +115,7 @@ export const useInspirat = singletonHook({} as InspiratHook, () => {
const [showDevTools, setShowDevTools] = React.useState(false);
const [isLoadingPhotos, setIsLoadingPhotos] = React.useState(false);
const [name, setName] = useStorageItem<string>('name');
const [hasSeenIntroduction, setHasSeenIntroduction] = useStorageItem<boolean>('hasSeenIntroduction');
const [hasSeenIntroduction, setHasSeenIntroduction] = useStorageItem<boolean>('hasSeenIntroduction', false);
const query = useQuery();


Expand Down
34 changes: 24 additions & 10 deletions packages/client/src/hooks/use-storage-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import useAsyncEffect from 'use-async-effect';

import storage from 'lib/storage';

/**
* @private
*
* Value we compare to to determine if a storage value is pending sync.
*/
const PENDING = Symbol('PENDING');


/**
* Returns true if the provided value is pending.
*/
export function isPending(value: any) {
return value === PENDING;
}


type HookReturnValue<T = any> = [
T,
Expand All @@ -15,27 +30,26 @@ type HookReturnValue<T = any> = [
*
* TODO: Make own package.
*/
function useStorageItem<T = any>(key: string): HookReturnValue<T | undefined>;
function useStorageItem<T = any>(key: string, initialValue: T): HookReturnValue<T>;
function useStorageItem<T = any>(key: string): HookReturnValue<T | typeof PENDING | undefined>;
function useStorageItem<T = any>(key: string, initialValue: T): HookReturnValue<T | typeof PENDING>;
function useStorageItem<T = any>(key: string, initialValue?: T) {
const [localValue, setLocalValue] = React.useState<T | undefined>(initialValue as T);
const [localValue, setLocalValue] = React.useState<T | typeof PENDING | undefined>(PENDING);

const setValue: React.Dispatch<React.SetStateAction<T>> = value => {
void storage.setItem(key, value);
setLocalValue(value as T);
};

useAsyncEffect(async isMounted => {
useAsyncEffect(async () => {
const valueFromStorage = await storage.getItem<T>(key);

if (valueFromStorage === null) {
void storage.setItem(key, initialValue);
}

if (isMounted() && valueFromStorage !== null) {
if (valueFromStorage !== null) {
setLocalValue(valueFromStorage);
} else if (initialValue !== undefined) {
setValue(initialValue);
}
}, []);

}, [setLocalValue]);

return [localValue, setValue];
}
Expand Down

0 comments on commit 199f8f3

Please sign in to comment.