signal-desktop/ts/hooks/useScrollLock.tsx

88 lines
2.1 KiB
TypeScript
Raw Normal View History

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useContext, createContext, useEffect, useRef } from 'react';
import * as log from '../logging/log';
export type ScrollerLock = Readonly<{
isLocked(): boolean;
lock(reason: string, onUserInterrupt: () => void): () => void;
onUserInterrupt(reason: string): void;
}>;
export function createScrollerLock(
title: string,
onUpdate: () => void
): ScrollerLock {
const locks = new Set<() => void>();
let lastUpdate: boolean | null = null;
function update() {
const isLocked = locks.size > 0;
if (isLocked !== lastUpdate) {
lastUpdate = isLocked;
onUpdate();
}
}
return {
isLocked() {
return locks.size > 0;
},
lock(reason, onUserInterrupt) {
log.info('ScrollerLock: Locking', title, reason);
locks.add(onUserInterrupt);
update();
function release() {
log.info('ScrollerLock: Releasing', title, reason);
locks.delete(onUserInterrupt);
update();
}
return release;
},
onUserInterrupt(reason) {
// Ignore interuptions if we're not locked
if (locks.size > 0) {
log.info('ScrollerLock: User Interrupt', title, reason);
locks.forEach(listener => listener());
locks.clear();
update();
}
},
};
}
export const ScrollerLockContext = createContext<ScrollerLock | null>(null);
export type ScrollLockProps = Readonly<{
reason: string;
lockScrollWhen: boolean;
onUserInterrupt(): void;
}>;
export function useScrollerLock({
reason,
lockScrollWhen,
onUserInterrupt,
}: ScrollLockProps): void {
const scrollerLock = useContext(ScrollerLockContext);
if (scrollerLock == null) {
throw new Error('Missing <ScrollLockProvider/>');
}
const onUserInterruptRef = useRef(onUserInterrupt);
useEffect(() => {
onUserInterruptRef.current = onUserInterrupt;
}, [onUserInterrupt]);
useEffect(() => {
if (lockScrollWhen) {
return scrollerLock.lock(reason, () => {
onUserInterruptRef.current();
});
}
return undefined;
}, [reason, scrollerLock, lockScrollWhen, onUserInterrupt]);
}