2021-01-08 18:58:28 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-01-17 22:23:19 +00:00
|
|
|
import * as React from 'react';
|
2020-05-05 19:49:34 +00:00
|
|
|
import { ActionCreatorsMapObject, bindActionCreators } from 'redux';
|
|
|
|
import { useDispatch } from 'react-redux';
|
2020-01-17 22:23:19 +00:00
|
|
|
|
2021-03-19 00:22:17 +00:00
|
|
|
export function usePrevious<T>(initialValue: T, currentValue: T): T {
|
|
|
|
const previousValueRef = React.useRef<T>(initialValue);
|
|
|
|
const result = previousValueRef.current;
|
|
|
|
previousValueRef.current = currentValue;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-01-17 22:23:19 +00:00
|
|
|
// Restore focus on teardown
|
|
|
|
export const useRestoreFocus = (
|
|
|
|
// The ref for the element to receive initial focus
|
2020-09-14 21:56:35 +00:00
|
|
|
focusRef: React.RefObject<HTMLElement>,
|
2020-01-17 22:23:19 +00:00
|
|
|
// Allow for an optional root element that must exist
|
|
|
|
root: boolean | HTMLElement | null = true
|
2020-09-14 21:56:35 +00:00
|
|
|
): void => {
|
2020-01-17 22:23:19 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
if (!root) {
|
2020-09-14 21:56:35 +00:00
|
|
|
return undefined;
|
2020-01-17 22:23:19 +00:00
|
|
|
}
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
const lastFocused = document.activeElement as HTMLElement;
|
|
|
|
|
2020-01-17 22:23:19 +00:00
|
|
|
if (focusRef.current) {
|
|
|
|
focusRef.current.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
2020-02-27 02:09:19 +00:00
|
|
|
// This ensures that the focus is returned to
|
|
|
|
// previous element
|
|
|
|
setTimeout(() => {
|
|
|
|
if (lastFocused && lastFocused.focus) {
|
|
|
|
lastFocused.focus();
|
|
|
|
}
|
|
|
|
});
|
2020-01-17 22:23:19 +00:00
|
|
|
};
|
|
|
|
}, [focusRef, root]);
|
|
|
|
};
|
2020-05-05 19:49:34 +00:00
|
|
|
|
|
|
|
export const useBoundActions = <T extends ActionCreatorsMapObject>(
|
|
|
|
actions: T
|
2020-09-14 21:56:35 +00:00
|
|
|
): T => {
|
2020-05-05 19:49:34 +00:00
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
|
|
|
return React.useMemo(() => {
|
|
|
|
return bindActionCreators(actions, dispatch);
|
2020-09-14 21:56:35 +00:00
|
|
|
}, [actions, dispatch]);
|
2020-05-05 19:49:34 +00:00
|
|
|
};
|
2020-12-02 01:52:01 +00:00
|
|
|
|
|
|
|
export const usePageVisibility = (): boolean => {
|
|
|
|
const [result, setResult] = React.useState(!document.hidden);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
const onVisibilityChange = () => {
|
|
|
|
setResult(!document.hidden);
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange', onVisibilityChange, false);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener(
|
|
|
|
'visibilitychange',
|
|
|
|
onVisibilityChange,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
2021-01-08 18:58:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A light hook wrapper around `IntersectionObserver`.
|
|
|
|
*
|
|
|
|
* Example usage:
|
|
|
|
*
|
|
|
|
* function MyComponent() {
|
|
|
|
* const [intersectionRef, intersectionEntry] = useIntersectionObserver();
|
|
|
|
* const isVisible = intersectionEntry
|
|
|
|
* ? intersectionEntry.isIntersecting
|
|
|
|
* : true;
|
|
|
|
*
|
|
|
|
* return (
|
|
|
|
* <div ref={intersectionRef}>
|
|
|
|
* I am {isVisible ? 'on the screen' : 'invisible'}
|
|
|
|
* </div>
|
|
|
|
* );
|
|
|
|
* }
|
|
|
|
*/
|
|
|
|
export function useIntersectionObserver(): [
|
|
|
|
(el?: Element | null) => void,
|
|
|
|
IntersectionObserverEntry | null
|
|
|
|
] {
|
|
|
|
const [
|
|
|
|
intersectionObserverEntry,
|
|
|
|
setIntersectionObserverEntry,
|
|
|
|
] = React.useState<IntersectionObserverEntry | null>(null);
|
|
|
|
|
|
|
|
const unobserveRef = React.useRef<(() => unknown) | null>(null);
|
|
|
|
|
|
|
|
const setRef = React.useCallback((el?: Element | null) => {
|
|
|
|
if (unobserveRef.current) {
|
|
|
|
unobserveRef.current();
|
|
|
|
unobserveRef.current = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(entries => {
|
|
|
|
if (entries.length !== 1) {
|
|
|
|
window.log.error(
|
|
|
|
'IntersectionObserverWrapper was observing the wrong number of elements'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
entries.forEach(entry => {
|
|
|
|
setIntersectionObserverEntry(entry);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
unobserveRef.current = observer.unobserve.bind(observer, el);
|
|
|
|
|
|
|
|
observer.observe(el);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return [setRef, intersectionObserverEntry];
|
|
|
|
}
|