diff --git a/ts/components/CallManager.dom.tsx b/ts/components/CallManager.dom.tsx index bd2df13f17..92f3fc1481 100644 --- a/ts/components/CallManager.dom.tsx +++ b/ts/components/CallManager.dom.tsx @@ -55,7 +55,7 @@ import { createLogger } from '../logging/log.std.ts'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.std.ts'; import { CallingAdhocCallInfo } from './CallingAdhocCallInfo.dom.tsx'; import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl.std.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { copyCallLink } from '../util/copyLinksWithToast.dom.ts'; import { redactNotificationProfileId, @@ -264,7 +264,10 @@ function ActiveCallManager({ // For caching screenshare frames which update slowly, between Pip and CallScreen. const imageDataCache = useRef(new Map()); - const previousConversationId = usePrevious(conversation.id, conversation.id); + const previousConversationId = usePreviousDeprecated( + conversation.id, + conversation.id + ); useEffect(() => { if (conversation.id !== previousConversationId) { imageDataCache.current.clear(); diff --git a/ts/components/CallScreen.dom.tsx b/ts/components/CallScreen.dom.tsx index 5853d3fd3f..9ea02d9ad3 100644 --- a/ts/components/CallScreen.dom.tsx +++ b/ts/components/CallScreen.dom.tsx @@ -80,7 +80,7 @@ import { } from '../hooks/useKeyboardShortcuts.dom.tsx'; import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate.std.ts'; import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting.std.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { CallingToastProvider, PersistentCallingToast, @@ -585,7 +585,7 @@ export function CallScreen({ }, [isSendingVideo, handleSize, isLonelyInCall, setLocalPreviewContainer]); const { selfViewExpanded } = activeCall; - const previousSelfViewExpanded = usePrevious( + const previousSelfViewExpanded = usePreviousDeprecated( selfViewExpanded, selfViewExpanded ); @@ -777,7 +777,10 @@ export function CallScreen({ const [localHandRaised, setLocalHandRaised] = useState( syncedLocalHandRaised ); - const previousLocalHandRaised = usePrevious(localHandRaised, localHandRaised); + const previousLocalHandRaised = usePreviousDeprecated( + localHandRaised, + localHandRaised + ); const toggleRaiseHand = useCallback( (raise?: boolean) => { const nextValue = raise ?? !localHandRaised; @@ -1339,7 +1342,7 @@ function useViewModeChangedToast({ i18n: LocalizerType; }): void { const { viewMode } = activeCall; - const previousViewMode = usePrevious(viewMode, viewMode); + const previousViewMode = usePreviousDeprecated(viewMode, viewMode); const presenterAci = usePresenter(activeCall.remoteParticipants); const VIEW_MODE_CHANGED_TOAST_KEY = 'view-mode-changed'; diff --git a/ts/components/CallingPip.dom.tsx b/ts/components/CallingPip.dom.tsx index 51ac159516..63904125d5 100644 --- a/ts/components/CallingPip.dom.tsx +++ b/ts/components/CallingPip.dom.tsx @@ -36,7 +36,7 @@ import type { ConversationType } from '../state/ducks/conversations.preload.ts'; import { Avatar, AvatarSize } from './Avatar.dom.tsx'; import { AvatarColors } from '../types/Colors.std.ts'; import type { SetLocalPreviewContainerType } from '../services/calling.preload.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import type { SizeCallbackType } from '../calling/VideoSupport.preload.ts'; import { MAX_FRAME_HEIGHT } from '../calling/constants.std.ts'; @@ -290,7 +290,10 @@ export function CallingPip({ }; }, []); - const previousIsWindowLarge = usePrevious(isWindowLarge, isWindowLarge); + const previousIsWindowLarge = usePreviousDeprecated( + isWindowLarge, + isWindowLarge + ); // This only runs when isWindowLarge changes, so we aggressively change height + width useEffect(() => { if (previousIsWindowLarge === isWindowLarge) { diff --git a/ts/components/CallingRaisedHandsList.dom.tsx b/ts/components/CallingRaisedHandsList.dom.tsx index b943f3015a..209c8ac8c4 100644 --- a/ts/components/CallingRaisedHandsList.dom.tsx +++ b/ts/components/CallingRaisedHandsList.dom.tsx @@ -13,7 +13,7 @@ import type { ConversationType } from '../state/ducks/conversations.preload.ts'; import { ModalHost } from './ModalHost.dom.tsx'; import { drop } from '../util/drop.std.ts'; import { createLogger } from '../logging/log.std.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePrevious, usePreviousEffect } from '../hooks/usePrevious.std.ts'; import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts'; const log = createLogger('CallingRaisedHandsList'); @@ -200,24 +200,20 @@ export function CallingRaisedHandsListButton({ [] ); - const prevRaisedHandsCount = usePrevious(raisedHandsCount, raisedHandsCount); - const prevSyncedLocalHandRaised = usePrevious( + const prevRaisedHandsCount = usePrevious(raisedHandsCount) ?? 0; + const prevSyncedLocalHandRaised = usePreviousEffect( syncedLocalHandRaised, syncedLocalHandRaised ); + const prevShownRaisedHandsCountRef = useRef(raisedHandsCount); const prevShownSyncedLocalHandRaisedRef = useRef( syncedLocalHandRaised ); + // Bouncy effect useEffect(() => { - if ( - raisedHandsCount > prevRaisedHandsCount || - (raisedHandsCount > 0 && !isVisible) - ) { - setIsVisible(true); - opacitySpringApi.stop(); - drop(Promise.all(opacitySpringApi.start({ opacity: 1 }))); + if (raisedHandsCount > prevRaisedHandsCount) { scaleSpringApi.stop(); drop( Promise.all( @@ -228,14 +224,33 @@ export function CallingRaisedHandsListButton({ }) ) ); - } else if (raisedHandsCount === 0) { - opacitySpringApi.stop(); + } + }, [raisedHandsCount, prevRaisedHandsCount, scaleSpringApi]); + + useEffect(() => { + if (raisedHandsCount === prevRaisedHandsCount) { + return; + } + + opacitySpringApi.stop(); + if (raisedHandsCount > 0) { + setIsVisible(true); drop( Promise.all( opacitySpringApi.start({ + from: { opacity: opacitySpringProps.opacity }, + to: { opacity: 1 }, + }) + ) + ); + } else { + drop( + Promise.all( + opacitySpringApi.start({ + from: { opacity: opacitySpringProps.opacity }, to: { opacity: 0 }, - onRest: () => { - if (!raisedHandsCount) { + onResolve: ({ cancelled }) => { + if (!cancelled) { setIsVisible(false); } }, @@ -244,11 +259,10 @@ export function CallingRaisedHandsListButton({ ); } }, [ - isVisible, raisedHandsCount, prevRaisedHandsCount, opacitySpringApi, - scaleSpringApi, + opacitySpringProps.opacity, setIsVisible, ]); diff --git a/ts/components/CallingToast.dom.tsx b/ts/components/CallingToast.dom.tsx index 4a624f6ca3..e77ecd213e 100644 --- a/ts/components/CallingToast.dom.tsx +++ b/ts/components/CallingToast.dom.tsx @@ -21,7 +21,7 @@ import classNames from 'classnames'; import { v4 as uuid } from 'uuid'; import { useIsMounted } from '../hooks/useIsMounted.std.ts'; import type { LocalizerType } from '../types/I18N.std.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { difference } from '../util/setUtil.std.ts'; import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts'; @@ -81,7 +81,7 @@ export function CallingToastProvider({ transitionFrom?: object; }): JSX.Element { const [toasts, setToasts] = useState>([]); - const previousToasts = usePrevious([], toasts); + const previousToasts = usePreviousDeprecated([], toasts); const timeouts = useRef>(new Map()); // All toasts are paused on hover or focus so that toasts don't disappear while a user // is attempting to interact with them diff --git a/ts/components/CallingToastManager.dom.tsx b/ts/components/CallingToastManager.dom.tsx index 4f262e5611..8df6184b99 100644 --- a/ts/components/CallingToastManager.dom.tsx +++ b/ts/components/CallingToastManager.dom.tsx @@ -10,7 +10,7 @@ import { CallMode } from '../types/CallDisposition.std.ts'; import type { ConversationType } from '../state/ducks/conversations.preload.ts'; import type { LocalizerType } from '../types/Util.std.ts'; import { CallingToastProvider, useCallingToasts } from './CallingToast.dom.tsx'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { difference as setDifference } from '../util/setUtil.std.ts'; import { isMoreRecentThan } from '../util/timestamp.std.ts'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.std.ts'; @@ -58,7 +58,10 @@ export function useScreenSharingStoppedToast({ () => getCurrentPresenter(activeCall), [activeCall] ); - const previousPresenter = usePrevious(currentPresenter, currentPresenter); + const previousPresenter = usePreviousDeprecated( + currentPresenter, + currentPresenter + ); useEffect(() => { if (previousPresenter && !currentPresenter) { @@ -93,7 +96,10 @@ function useMutedToast({ mutedBy: number | undefined; i18n: LocalizerType; }): void { - const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio); + const previousHasLocalAudio = usePreviousDeprecated( + hasLocalAudio, + hasLocalAudio + ); const { showToast, hideToast } = useCallingToasts(); const MUTED_TOAST_KEY = 'muted'; @@ -131,7 +137,10 @@ function useOutgoingRingToast({ i18n: LocalizerType; }): void { const { showToast, hideToast } = useCallingToasts(); - const previousOutgoingRing = usePrevious(outgoingRing, outgoingRing); + const previousOutgoingRing = usePreviousDeprecated( + outgoingRing, + outgoingRing + ); const RINGING_TOAST_KEY = 'ringing'; useEffect(() => { @@ -182,7 +191,7 @@ function useRaisedHandsToast({ return () => clearTimeout(timeout); }, []); - const previousRaisedHands = usePrevious(raisedHands, raisedHands); + const previousRaisedHands = usePreviousDeprecated(raisedHands, raisedHands); const [newHands, loweredHands]: [Set, Set] = isLoaded ? [ setDifference( @@ -276,7 +285,7 @@ function useLowerHandSuggestionToast({ handleLowerHand: (() => void) | undefined; isHandRaised: boolean | undefined; }): void { - const previousSuggestLowerHand = usePrevious( + const previousSuggestLowerHand = usePreviousDeprecated( suggestLowerHand, suggestLowerHand ); @@ -343,7 +352,7 @@ function useMutedByToast({ conversationsByDemuxId?: Map; i18n: LocalizerType; }): void { - const previousMutedBy = usePrevious(mutedBy, mutedBy); + const previousMutedBy = usePreviousDeprecated(mutedBy, mutedBy); const { showToast, hideToast } = useCallingToasts(); const MUTED_BY_TOAST_KEY = 'MUTED_BY_TOAST_KEY'; @@ -401,7 +410,7 @@ function useObservedRemoteMuteToast({ }): void { const { showToast, hideToast } = useCallingToasts(); const OBSERVED_REMOTE_MUTE_TOAST_KEY = 'OBSERVED_REMOTE_MUTE_TOAST_KEY'; - const previousObservedRemoteMute = usePrevious( + const previousObservedRemoteMute = usePreviousDeprecated( observedRemoteMute, observedRemoteMute ); diff --git a/ts/components/CompositionArea.dom.tsx b/ts/components/CompositionArea.dom.tsx index ba93ed6ef5..8586d9152e 100644 --- a/ts/components/CompositionArea.dom.tsx +++ b/ts/components/CompositionArea.dom.tsx @@ -66,7 +66,7 @@ import { import { MediaEditor } from './MediaEditor.dom.tsx'; import { isImageTypeSupported } from '../util/GoogleChrome.std.ts'; import * as KeyboardLayout from '../services/keyboardLayout.dom.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { PanelType } from '../types/Panels.std.ts'; import type { SmartCompositionRecordingDraftProps } from '../state/smart/CompositionRecordingDraft.preload.tsx'; import { useEscapeHandling } from '../hooks/useEscapeHandling.dom.ts'; @@ -527,7 +527,7 @@ export const CompositionArea = memo(function CompositionArea({ }); // Focus input on first mount - const previousFocusCounter = usePrevious( + const previousFocusCounter = usePreviousDeprecated( focusCounter, focusCounter ); @@ -543,8 +543,11 @@ export const CompositionArea = memo(function CompositionArea({ } }, [inputApiRef, focusCounter, previousFocusCounter]); - const previousSendCounter = usePrevious(sendCounter, sendCounter); - const previousConversationId = usePrevious(conversationId, conversationId); + const previousSendCounter = usePreviousDeprecated(sendCounter, sendCounter); + const previousConversationId = usePreviousDeprecated( + conversationId, + conversationId + ); useEffect(() => { if (!inputApiRef.current) { return; @@ -569,9 +572,10 @@ export const CompositionArea = memo(function CompositionArea({ // - User begins editing another message. const editHistoryLength = draftEditMessage?.editHistoryLength; const hasEditHistoryChanged = - usePrevious(editHistoryLength, editHistoryLength) !== editHistoryLength; + usePreviousDeprecated(editHistoryLength, editHistoryLength) !== + editHistoryLength; const hasEditedMessageChanged = - usePrevious(editedMessageId, editedMessageId) !== editedMessageId; + usePreviousDeprecated(editedMessageId, editedMessageId) !== editedMessageId; const hasEditDraftChanged = hasEditHistoryChanged || hasEditedMessageChanged; useEffect(() => { diff --git a/ts/components/CompositionInput.dom.tsx b/ts/components/CompositionInput.dom.tsx index da5556c88c..e983e76b7d 100644 --- a/ts/components/CompositionInput.dom.tsx +++ b/ts/components/CompositionInput.dom.tsx @@ -75,7 +75,7 @@ import { createLogger } from '../logging/log.std.ts'; import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.std.ts'; import { StagedLinkPreview } from './conversation/StagedLinkPreview.dom.tsx'; import type { DraftEditMessageType } from '../model-types.d.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { matchBold, matchItalic, @@ -417,11 +417,11 @@ export function CompositionInput(props: Props): ReactElement { return false; }; - const previousFormattingEnabled = usePrevious( + const previousFormattingEnabled = usePreviousDeprecated( isFormattingEnabled, isFormattingEnabled ); - const previousIsMouseDown = usePrevious(isMouseDown, isMouseDown); + const previousIsMouseDown = usePreviousDeprecated(isMouseDown, isMouseDown); useEffect(() => { const formattingChanged = @@ -779,7 +779,7 @@ export function CompositionInput(props: Props): ReactElement { const memberIdList = useMemo(() => { return JSON.stringify(sortedGroupMembers?.map(mem => mem.id)); }, [sortedGroupMembers]); - const previousMemberIdList = usePrevious(undefined, memberIdList); + const previousMemberIdList = usePreviousDeprecated(undefined, memberIdList); useEffect(() => { memberRepositoryRef.current.updateMembers(sortedGroupMembers || []); diff --git a/ts/components/ContactPills.dom.tsx b/ts/components/ContactPills.dom.tsx index 1287528232..c5618ed600 100644 --- a/ts/components/ContactPills.dom.tsx +++ b/ts/components/ContactPills.dom.tsx @@ -5,7 +5,7 @@ import type { ReactNode, JSX } from 'react'; import { useRef, useEffect, Children } from 'react'; import classNames from 'classnames'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { scrollToBottom } from '../util/scrollUtil.std.ts'; type PropsType = { @@ -21,7 +21,7 @@ export function ContactPills({ // oxlint-disable-next-line no-react-children const childCount = Children.count(children); - const previousChildCount = usePrevious(0, childCount); + const previousChildCount = usePreviousDeprecated(0, childCount); useEffect(() => { const hasAddedNewChild = childCount > previousChildCount; diff --git a/ts/components/GroupCallRemoteParticipant.dom.tsx b/ts/components/GroupCallRemoteParticipant.dom.tsx index a606f7e55b..532e5ca353 100644 --- a/ts/components/GroupCallRemoteParticipant.dom.tsx +++ b/ts/components/GroupCallRemoteParticipant.dom.tsx @@ -30,7 +30,7 @@ import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants.std.ts'; import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate.std.ts'; import { isOlderThan } from '../util/timestamp.std.ts'; import type { CallingImageDataCache } from './CallManager.dom.tsx'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx'; import type { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.tsx'; import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx'; @@ -129,8 +129,11 @@ export const GroupCallRemoteParticipant: FC = memo( !props.isInPip ? props.audioLevel > 0 : false, SPEAKING_LINGER_MS ); - const previousSharingScreen = usePrevious(sharingScreen, sharingScreen); - const prevIsActiveSpeakerInSpeakerView = usePrevious( + const previousSharingScreen = usePreviousDeprecated( + sharingScreen, + sharingScreen + ); + const prevIsActiveSpeakerInSpeakerView = usePreviousDeprecated( isActiveSpeakerInSpeakerView, isActiveSpeakerInSpeakerView ); diff --git a/ts/components/LeftPane.dom.tsx b/ts/components/LeftPane.dom.tsx index 86061751fa..e15e69ca33 100644 --- a/ts/components/LeftPane.dom.tsx +++ b/ts/components/LeftPane.dom.tsx @@ -35,7 +35,7 @@ import { LeftPaneMode } from '../types/leftPane.std.ts'; import type { LocalizerType, ThemeType } from '../types/Util.std.ts'; import { ScrollBehavior } from '../types/Util.std.ts'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { missingCaseError } from '../util/missingCaseError.std.ts'; import type { DurationInSeconds } from '../util/durations/index.std.ts'; import { WidthBreakpoint, getNavSidebarWidthBreakpoint } from './_util.std.ts'; @@ -312,7 +312,7 @@ export function LeftPane({ dismissBackupMediaDownloadBanner, updateFilterByUnread, }: PropsType): JSX.Element { - const previousModeSpecificProps = usePrevious( + const previousModeSpecificProps = usePreviousDeprecated( modeSpecificProps, modeSpecificProps ); @@ -599,7 +599,7 @@ export function LeftPane({ const measureRef = useRef(null); const measureSize = useSizeObserver(measureRef); - const previousMeasureSize = usePrevious(null, measureSize); + const previousMeasureSize = usePreviousDeprecated(null, measureSize); const widthBreakpoint = getNavSidebarWidthBreakpoint( measureSize && !measureSize.hidden @@ -613,7 +613,7 @@ export function LeftPane({ }; // Control scroll position - const previousSelectedConversationId = usePrevious( + const previousSelectedConversationId = usePreviousDeprecated( selectedConversationId, selectedConversationId ); diff --git a/ts/components/LeftPaneSearchInput.dom.tsx b/ts/components/LeftPaneSearchInput.dom.tsx index 7d46cbe232..5422433db9 100644 --- a/ts/components/LeftPaneSearchInput.dom.tsx +++ b/ts/components/LeftPaneSearchInput.dom.tsx @@ -10,7 +10,7 @@ import type { import type { LocalizerType } from '../types/Util.std.ts'; import { Avatar, AvatarSize } from './Avatar.dom.tsx'; import { SearchInput } from './SearchInput.dom.tsx'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { Tooltip, TooltipPlacement } from './Tooltip.dom.tsx'; import { Theme } from '../util/theme.std.ts'; @@ -67,12 +67,18 @@ export function LeftPaneSearchInput({ }: PropsType): JSX.Element { const inputRef = useRef(null); - const prevSearchConversationId = usePrevious( + const prevSearchConversationId = usePreviousDeprecated( undefined, searchConversation?.id ); - const prevSearchCounter = usePrevious(startSearchCounter, startSearchCounter); - const wasSearchingGlobally = usePrevious(false, isSearchingGlobally); + const prevSearchCounter = usePreviousDeprecated( + startSearchCounter, + startSearchCounter + ); + const wasSearchingGlobally = usePreviousDeprecated( + false, + isSearchingGlobally + ); useEffect(() => { // When user chooses to search in a given conversation we focus the field for them diff --git a/ts/components/Lightbox.dom.tsx b/ts/components/Lightbox.dom.tsx index d61b880283..7878f47d6d 100644 --- a/ts/components/Lightbox.dom.tsx +++ b/ts/components/Lightbox.dom.tsx @@ -29,7 +29,7 @@ import { formatDateTimeForAttachment } from '../util/formatTimestamp.dom.ts'; import { formatDuration } from '../util/formatDuration.std.ts'; import { isGIF, isIncremental } from '../util/Attachment.std.ts'; import { useRestoreFocus } from '../hooks/useRestoreFocus.dom.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { arrow } from '../util/keyboard.dom.ts'; import { drop } from '../util/drop.std.ts'; import { isCmdOrCtrl } from '../hooks/useKeyboardShortcuts.dom.tsx'; @@ -117,7 +117,7 @@ export function Lightbox({ }: PropsType): JSX.Element | null { const hasThumbnails = media.length > 1; const messageId = media.at(0)?.message.id; - const prevMessageId = usePrevious(messageId, messageId); + const prevMessageId = usePreviousDeprecated(messageId, messageId); const needsAnimation = messageId !== prevMessageId; const [root, setRoot] = useState(); diff --git a/ts/components/ModalHost.dom.tsx b/ts/components/ModalHost.dom.tsx index 78ef95aed0..c388a33f8f 100644 --- a/ts/components/ModalHost.dom.tsx +++ b/ts/components/ModalHost.dom.tsx @@ -21,7 +21,7 @@ import { assertDev } from '../util/assert.std.ts'; import { getClassNamesFor } from '../util/getClassNamesFor.std.ts'; import { themeClassName } from '../util/theme.std.ts'; import { useEscapeHandling } from '../hooks/useEscapeHandling.dom.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { handleOutsideClick } from '../util/handleOutsideClick.dom.ts'; import { createLogger } from '../logging/log.std.ts'; @@ -57,7 +57,7 @@ export const ModalHost = memo(function ModalHostInner({ theme, }: PropsType) { const containerRef = useRef(null); - const previousModalName = usePrevious(modalName, modalName); + const previousModalName = usePreviousDeprecated(modalName, modalName); const modalContainer = useContext(ModalContainerContext) ?? document.body; if (previousModalName !== modalName) { diff --git a/ts/components/PreferencesDonateFlow.dom.tsx b/ts/components/PreferencesDonateFlow.dom.tsx index 9b57dfbd21..a13e2ad6a2 100644 --- a/ts/components/PreferencesDonateFlow.dom.tsx +++ b/ts/components/PreferencesDonateFlow.dom.tsx @@ -72,7 +72,7 @@ import { offsetDistanceModifier } from '../util/popperUtil.std.ts'; import { AxoButton } from '../axo/AxoButton.dom.tsx'; import { missingCaseError } from '../util/missingCaseError.std.ts'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser.dom.ts'; -import { usePrevious } from '../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../hooks/usePrevious.std.ts'; import { tw } from '../axo/tw.dom.tsx'; const SUPPORT_URL = 'https://support.signal.org/hc/requests/new?desktop'; @@ -168,7 +168,7 @@ export function PreferencesDonateFlow({ CardFormValues | undefined >(); - const prevStep = usePrevious(step, step); + const prevStep = usePreviousDeprecated(step, step); const hasCardFormData = useMemo(() => { if (!cardFormValues) { diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx index ef0c162537..5af23f27b6 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx @@ -35,7 +35,7 @@ import type { import type { LocalizerType, ThemeType } from '../../../types/Util.std.ts'; import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.ts'; import type { Location } from '../../../types/Nav.std.ts'; -import { usePrevious } from '../../../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../../../hooks/usePrevious.std.ts'; import type { Emoji } from '../../../axo/emoji.std.ts'; export type PropsDataType = { @@ -148,7 +148,7 @@ export function GroupMemberLabelEditor({ // Popping the panel here after a save is far safer; we may not have re-rendered with // the new existing values yet when the onSuccess callback down-file is called. - const previousIsSaving = usePrevious(isSaving, isSaving); + const previousIsSaving = usePreviousDeprecated(isSaving, isSaving); useEffect(() => { if (!isSaving && previousIsSaving !== isSaving && !isDirty) { popPanelForConversation(); diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx index a389685556..e37847b421 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx @@ -1,7 +1,7 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ForwardedRef, ReactNode, JSX } from 'react'; -import { forwardRef, memo, useCallback, useMemo, useState } from 'react'; +import { forwardRef, memo, useCallback, useMemo } from 'react'; import { Tabs } from 'radix-ui'; import { AnimatePresence, motion } from 'motion/react'; import type { LocalizerType } from '../../../types/I18N.std.ts'; @@ -19,6 +19,7 @@ import type { HydratedBodyRangesType } from '../../../types/BodyRange.std.ts'; import { AxoSymbol } from '../../../axo/AxoSymbol.dom.tsx'; import { missingCaseError } from '../../../util/missingCaseError.std.ts'; import { stripNewlinesForLeftPane } from '../../../util/stripNewlinesForLeftPane.std.ts'; +import { usePrevious } from '../../../hooks/usePrevious.std.ts'; enum Direction { None = 0, @@ -26,18 +27,6 @@ enum Direction { Forwards = 1, } -// This `usePrevious()` hook is safe in React concurrent mode and doesn't break -// when rendered multiple times with the same values in `` -function usePrevious(value: T): T | null { - const [current, setCurrent] = useState(value); - const [previous, setPrevious] = useState(null); - if (current !== value) { - setCurrent(value); - setPrevious(current); - } - return previous; -} - export type PinMessageText = Readonly<{ body: string; bodyRanges: HydratedBodyRangesType; diff --git a/ts/components/conversation/poll-message/PollMessageContents.dom.tsx b/ts/components/conversation/poll-message/PollMessageContents.dom.tsx index bc3a0a2fba..eaea2b66eb 100644 --- a/ts/components/conversation/poll-message/PollMessageContents.dom.tsx +++ b/ts/components/conversation/poll-message/PollMessageContents.dom.tsx @@ -12,7 +12,7 @@ import type { PollWithResolvedVotersType } from '../../../state/selectors/messag import type { LocalizerType } from '../../../types/Util.std.ts'; import { PollVotesModal } from './PollVotesModal.dom.tsx'; import { SpinnerV2 } from '../../SpinnerV2.dom.tsx'; -import { usePrevious } from '../../../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../../../hooks/usePrevious.std.ts'; import { UserText } from '../../UserText.dom.tsx'; function VotedCheckmark({ @@ -170,7 +170,10 @@ export function PollMessageContents({ const [isPending, setIsPending] = useState(false); const hasPendingVotes = poll.pendingVoteDiff && poll.pendingVoteDiff.size > 0; - const hadPendingVotesInLastRender = usePrevious(hasPendingVotes, undefined); + const hadPendingVotesInLastRender = usePreviousDeprecated( + hasPendingVotes, + undefined + ); const pendingCheckTimer = useRef(null); const isIncoming = direction === 'incoming'; diff --git a/ts/hooks/useActivateSpeakerViewOnPresenting.std.ts b/ts/hooks/useActivateSpeakerViewOnPresenting.std.ts index 161b7a61f7..467f93bbfd 100644 --- a/ts/hooks/useActivateSpeakerViewOnPresenting.std.ts +++ b/ts/hooks/useActivateSpeakerViewOnPresenting.std.ts @@ -3,7 +3,7 @@ import { useEffect, useMemo } from 'react'; import type { AciString } from '../types/ServiceId.std.ts'; -import { usePrevious } from './usePrevious.std.ts'; +import { usePreviousDeprecated } from './usePrevious.std.ts'; type RemoteParticipant = { hasRemoteVideo: boolean; @@ -31,7 +31,7 @@ export function useActivateSpeakerViewOnPresenting({ switchFromPresentationView: () => void; }): void { const presenterAci = usePresenter(remoteParticipants); - const prevPresenterAci = usePrevious(presenterAci, presenterAci); + const prevPresenterAci = usePreviousDeprecated(presenterAci, presenterAci); useEffect(() => { if (prevPresenterAci !== presenterAci && presenterAci) { diff --git a/ts/hooks/usePrevious.std.ts b/ts/hooks/usePrevious.std.ts index bc9ca876f1..98fe6d608b 100644 --- a/ts/hooks/usePrevious.std.ts +++ b/ts/hooks/usePrevious.std.ts @@ -1,11 +1,51 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; -export function usePrevious(initialValue: T, currentValue: T): T { +/** + * This `usePrevious()` hook is safe in React concurrent mode and doesn't break + * when rendered multiple times with the same values in `` + * Note: The previous value only updates when the value changes. + * If you want to do work once after a change and track that it was done: + * ``` + * const [counter, setCounter] = useState(0); + * const lastAnimatedRef = useRef(); + * + * useEffect(() => { + * if (counter === lastAnimatedRef.current) { + * return; + * } + * lastAnimatedRef.current = counter; + * // animate + * }, [counter]); + * ``` + */ +export function usePrevious(value: T): T | null { + const [current, setCurrent] = useState(value); + const [previous, setPrevious] = useState(null); + if (current !== value) { + setCurrent(value); + setPrevious(current); + } + return previous; +} + +// TODO: DESKTOP-10151 +/** @deprecated */ +export function usePreviousDeprecated(initialValue: T, currentValue: T): T { const previousValueRef = useRef(initialValue); const result = previousValueRef.current; previousValueRef.current = currentValue; return result; } + +/** @deprecated */ +export function usePreviousEffect(initialValue: T, currentValue: T): T { + const previousValueRef = useRef(initialValue); + const result = previousValueRef.current; + useEffect(() => { + previousValueRef.current = currentValue; + }, [currentValue]); + return result; +} diff --git a/ts/hooks/useRetryStorySend.dom.tsx b/ts/hooks/useRetryStorySend.dom.tsx index fb5a30dba2..95972c0ba9 100644 --- a/ts/hooks/useRetryStorySend.dom.tsx +++ b/ts/hooks/useRetryStorySend.dom.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, type JSX } from 'react'; import type { LocalizerType } from '../types/Util.std.ts'; import { ResolvedSendStatus } from '../types/Stories.std.ts'; -import { usePrevious } from './usePrevious.std.ts'; +import { usePreviousDeprecated } from './usePrevious.std.ts'; import { AxoConfirmDialog } from '../axo/AxoConfirmDialog.dom.tsx'; export function useRetryStorySend( @@ -19,7 +19,7 @@ export function useRetryStorySend( const [hasSendFailedAlert, setHasSendFailedAlert] = useState(false); const [wasManuallyRetried, setWasManuallyRetried] = useState(false); - const previousSendStatus = usePrevious(sendStatus, sendStatus); + const previousSendStatus = usePreviousDeprecated(sendStatus, sendStatus); useEffect(() => { if (!wasManuallyRetried) { diff --git a/ts/state/smart/VoiceNotesPlaybackProvider.preload.tsx b/ts/state/smart/VoiceNotesPlaybackProvider.preload.tsx index b1f700b8dc..a4974ebcb2 100644 --- a/ts/state/smart/VoiceNotesPlaybackProvider.preload.tsx +++ b/ts/state/smart/VoiceNotesPlaybackProvider.preload.tsx @@ -19,7 +19,7 @@ import { getConversations } from '../selectors/conversations.dom.ts'; import { SeenStatus } from '../../MessageSeenStatus.std.ts'; import { markViewed } from '../ducks/conversations.preload.ts'; import * as Errors from '../../types/errors.std.ts'; -import { usePrevious } from '../../hooks/usePrevious.std.ts'; +import { usePreviousDeprecated } from '../../hooks/usePrevious.std.ts'; const log = createLogger('VoiceNotesPlaybackProvider'); @@ -35,7 +35,10 @@ export const SmartVoiceNotesPlaybackProvider = memo( const active = useSelector(selectAudioPlayerActive); const conversations = useSelector(getConversations); - const previousStartPosition = usePrevious(undefined, active?.startPosition); + const previousStartPosition = usePreviousDeprecated( + undefined, + active?.startPosition + ); const content = active?.content; let url: undefined | string;