Enable exhaustive deps in the repo (#9508)
* Enable exhaustive deps in the repo * Add tests for the new hooks * Update claude.md * Remove pre-commit overwrite
This commit is contained in:
parent
36439bd5f4
commit
cff12f7182
182 changed files with 1154 additions and 602 deletions
|
|
@ -237,7 +237,12 @@ Located at `libraries/@mattermost/`:
|
|||
- **Prefer const objects over enums** for better tree-shaking: `export const Status = { Pending: 0, Done: 1 } as const;` with companion type `export type Status = typeof Status[keyof typeof Status];`
|
||||
|
||||
### React Hooks
|
||||
- **Always explain why dependencies are omitted** from exhaustive-deps with specific comments
|
||||
- **`react-hooks/exhaustive-deps` is enforced as error** - incomplete or incorrect dependency arrays will fail CI. Fix deps or use the dedicated hooks below when the omission is intentional.
|
||||
- **Always explain why dependencies are omitted** from exhaustive-deps with specific comments (and prefer the dedicated hooks below for common cases).
|
||||
- **Run-on-mount effects**: Use **`useDidMount`** from `@hooks/did_mount` instead of `useEffect(callback, [])` with an eslint-disable. The hook encapsulates the "run only on mount" intent and the single exhaustive-deps exception.
|
||||
- **Initial value only (no updates)**: Use **`useInitialValue`** from `@hooks/initial_value` instead of `useMemo(factory, [])` with an eslint-disable when the value must be computed once and never recomputed.
|
||||
- **Nested property as dependency**: When the meaningful dependency is a nested property (e.g. `currentUser?.notifyProps`) because the parent object reference may not change when that property changes, depend on that property in the array and add an eslint-disable with a comment explaining why. See `useNotificationProps` and `useUserTimezoneProps` in `app/hooks/` for the pattern.
|
||||
- **`useDidUpdate`**: The hook takes a dependency list from the caller; the implementation uses an eslint-disable with the comment that dependencies are provided by the caller.
|
||||
- Don't over-optimize with `useMemo` - only use for expensive calculations or objects passed as props to children
|
||||
- Use `useCallback` for render functions passed to components, not for functions called within render
|
||||
- Move module-level constants outside components
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
|
||||
import React, {useEffect} from 'react';
|
||||
import React from 'react';
|
||||
import {defineMessages} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, type TextStyle, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
info: {
|
||||
|
|
@ -41,7 +42,7 @@ const AppVersion = ({isWrapped = true, textStyle = {}}: AppVersionProps) => {
|
|||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const willHide = Keyboard.addListener('keyboardDidHide', () => {
|
||||
opacity.value = 1;
|
||||
});
|
||||
|
|
@ -53,7 +54,7 @@ const AppVersion = ({isWrapped = true, textStyle = {}}: AppVersionProps) => {
|
|||
willHide.remove();
|
||||
willShow.remove();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
const appVersion = (
|
||||
<FormattedText
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const AtMentionItem = ({
|
|||
}: AtMentionItemProps) => {
|
||||
const completeMention = useCallback((u: UserModel | UserProfile) => {
|
||||
onPress?.(u.username);
|
||||
}, []);
|
||||
}, [onPress]);
|
||||
|
||||
return (
|
||||
<UserItem
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const AddMembersBox = ({
|
|||
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CHANNEL_ADD_MEMBERS, title, {channelId, inModal}, options);
|
||||
}, [intl, channelId, inModal, testID, displayName]);
|
||||
}, [intl, theme, displayName, inModal, channelId]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const MutedBox = ({channelId, containerStyle, isMuted, showSnackBar = false, tes
|
|||
const handleOnPress = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
toggleMuteChannel(serverUrl, channelId, showSnackBar);
|
||||
}, [channelId, isMuted, serverUrl, showSnackBar]);
|
||||
}, [channelId, serverUrl, showSnackBar]);
|
||||
|
||||
const muteActionTestId = isMuted ? `${testID}.unmute.action` : `${testID}.mute.action`;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const SetHeaderBox = ({channelId, containerStyle, isHeaderSet, inModal, testID}:
|
|||
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, headerOnly: true});
|
||||
}, [intl, channelId]);
|
||||
}, [intl, inModal, channelId]);
|
||||
|
||||
let text;
|
||||
if (isHeaderSet) {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ const ChannelBookmark = ({
|
|||
if (action === 'none' && bookmark.id) {
|
||||
dismissOverlay(bookmark.id);
|
||||
}
|
||||
|
||||
// We don't care about `bookmark.id` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [action]);
|
||||
|
||||
if (isDocumentFile) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {fetchUserByIdBatched} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
|
@ -49,11 +50,11 @@ const DmAvatar = ({
|
|||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (authorId && !author) {
|
||||
fetchUserByIdBatched(serverUrl, authorId);
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
if (author?.deleteAt) {
|
||||
return (
|
||||
<CompassIcon
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ const ChannelItem = ({
|
|||
|
||||
const handleOnPress = useCallback(() => {
|
||||
onPress(channel);
|
||||
}, [channel.id]);
|
||||
}, [channel, onPress]);
|
||||
|
||||
const textStyles = useMemo(() => [
|
||||
isBolded && !isMuted ? textStyle.bold : textStyle.regular,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const ImageFileOverlay = ({value}: ImageFileOverlayProps) => {
|
|||
style.moreImagesText,
|
||||
{fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale))},
|
||||
];
|
||||
}, [isTablet]);
|
||||
}, [dimensions.scale, isTablet, style]);
|
||||
|
||||
return (
|
||||
<View style={style.moreImagesWrapper}>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const TabletOptions = ({
|
|||
|
||||
const toggleOverlay = useCallback(() => {
|
||||
setShowOptions(false);
|
||||
}, []);
|
||||
}, [setShowOptions]);
|
||||
|
||||
const overlayStyle = useMemo(() => ({
|
||||
marginTop: openUp ? 0 : openDownMargin,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const FileResult = ({
|
|||
setShowOptions(true);
|
||||
onOptionsPress(fInfo);
|
||||
});
|
||||
}, []);
|
||||
}, [height, onOptionsPress]);
|
||||
|
||||
const handleSetAction = useCallback((action: GalleryAction) => {
|
||||
setAction(action);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {type IntlShape, useIntl} from 'react-intl';
|
||||
import {Text, View, type StyleProp, type TextStyle, type ViewStyle} from 'react-native';
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ import {Screens, View as ViewConstants} from '@constants';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getUserById} from '@queries/servers/user';
|
||||
|
|
@ -155,7 +156,9 @@ function AutoCompleteSelector({
|
|||
}), [title, dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect]));
|
||||
|
||||
// Handle the text for the default value.
|
||||
useEffect(() => {
|
||||
// We want to run this only in the first render, since it is only for the default value.
|
||||
// Future changes in the selected value will update the itemText accordingly.
|
||||
useDidMount(() => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -172,11 +175,7 @@ function AutoCompleteSelector({
|
|||
Promise.all(namePromises).then((names) => {
|
||||
setItemText(names.join(', '));
|
||||
});
|
||||
|
||||
// We want to run this only in the first render, since it is only for the default value.
|
||||
// Future changes in the selected value will update the itemText accordingly.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const inputStyle = useMemo(() => {
|
||||
const res: StyleProp<ViewStyle> = [style.input];
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...pro
|
|||
return function cleanup() {
|
||||
return null;
|
||||
};
|
||||
|
||||
// We don't care about `getFormattedRelativeTime` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ const InputAccessoryViewContainer = ({
|
|||
return {
|
||||
height: animatedHeight.value,
|
||||
};
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -488,10 +488,7 @@ export const KeyboardAwarePostDraftContainer = ({
|
|||
animated: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ref is not required to be in deps because it is a stable reference
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [listRef]);
|
||||
|
||||
// Android: Watch for emoji picker closing and restore scroll position when both height and bottomInset reach 0
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
|
|
@ -558,25 +555,35 @@ export const KeyboardAwarePostDraftContainer = ({
|
|||
updateValue: updateValueRef.current,
|
||||
updateCursorPosition: updateCursorPositionRef.current,
|
||||
registerPostInputCallbacks,
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// Only include non-shared-value dependencies that can actually change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}), [
|
||||
keyboardCurrentHeight,
|
||||
bottomInset,
|
||||
scrollOffset,
|
||||
keyboardHeight,
|
||||
scrollPosition,
|
||||
onScroll,
|
||||
postInputContainerHeight,
|
||||
inputRef,
|
||||
blurInput,
|
||||
focusInput,
|
||||
blurAndDismissKeyboard,
|
||||
isKeyboardFullyOpen,
|
||||
isKeyboardFullyClosed,
|
||||
isKeyboardInTransition,
|
||||
isInputAccessoryViewMode,
|
||||
showInputAccessoryView,
|
||||
setShowInputAccessoryView,
|
||||
lastKeyboardHeight,
|
||||
inputAccessoryViewAnimatedHeight,
|
||||
isTransitioningFromCustomView,
|
||||
closeInputAccessoryView,
|
||||
scrollToEnd,
|
||||
isEmojiSearchFocused,
|
||||
setIsEmojiSearchFocused,
|
||||
registerCursorPosition,
|
||||
preserveCursorPositionForEmojiPicker,
|
||||
clearCursorPositionPreservation,
|
||||
isInEmojiPickerTransition,
|
||||
getPreservedCursorPosition,
|
||||
registerPostInputCallbacks,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {type GestureResponderEvent, type StyleProp, StyleSheet, Text, type TextStyle, View} from 'react-native';
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
|
|||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import GroupModel from '@database/models/server/group';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown';
|
||||
import {bottomSheet, dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
|
|
@ -81,15 +82,12 @@ const AtMention = ({
|
|||
const group = useMemoMentionedGroup(groups, user, mentionName);
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
// Fetches and updates the local db store with the mention
|
||||
if (!user?.username && !group?.name) {
|
||||
fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName);
|
||||
}
|
||||
|
||||
// Only fetch the user or group on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const openUserProfile = () => {
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type Props = {
|
|||
|
||||
const MathView = (props: Props) => {
|
||||
const id = useId();
|
||||
const config = useMemo(() => ({id}), []);
|
||||
const config = useMemo(() => ({id}), [id]);
|
||||
|
||||
return (
|
||||
<RNMathView
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const NavigationSearch = forwardRef<SearchRef, Props>(({
|
|||
const onFocus = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
||||
hideHeader?.();
|
||||
searchProps.onFocus?.(e);
|
||||
}, [hideHeader, searchProps.onFocus]);
|
||||
}, [hideHeader, searchProps]);
|
||||
|
||||
const showEmitter = useCallback(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const OptionBox = ({
|
|||
}
|
||||
|
||||
return style;
|
||||
}, [activated, containerStyle, theme, isDestructive]);
|
||||
}, [styles, containerStyle, isDestructive, theme.dndIndicator, theme.buttonBg, activated]);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
if (activeIconName || activeText) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import Badge from '@components/badge';
|
||||
|
|
@ -9,6 +9,7 @@ import {Screens} from '@constants';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {subscribeAllServers} from '@database/subscription/servers';
|
||||
import {subscribeMentionsByServer} from '@database/subscription/unreads';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
import type ServersModel from '@typings/database/models/app/servers';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
|
@ -86,7 +87,7 @@ const OtherMentionsBadge = ({channelId}: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const subscription = subscribeAllServers(serversObserver);
|
||||
|
||||
return () => {
|
||||
|
|
@ -96,7 +97,7 @@ const OtherMentionsBadge = ({channelId}: Props) => {
|
|||
});
|
||||
subscriptions.clear();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ export default function DraftHandler(props: Props) {
|
|||
uploadErrorHandlers.current[file.clientId!] = DraftEditPostUploadManager.registerErrorHandler(file.clientId!, newUploadError);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't care about `newUploadError` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -312,9 +312,6 @@ export default function PostInput({
|
|||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isDismissingEmojiPicker,
|
||||
focusTimeoutRef,
|
||||
|
|
@ -322,13 +319,19 @@ export default function PostInput({
|
|||
isEmojiSearchFocused,
|
||||
setIsFocused,
|
||||
setIsEmojiSearchFocused,
|
||||
setShowInputAccessoryView,
|
||||
showInputAccessoryView,
|
||||
lastKeyboardHeight,
|
||||
setShowInputAccessoryView,
|
||||
updateCursorPosition,
|
||||
value.length,
|
||||
clearCursorPositionPreservation,
|
||||
value,
|
||||
cursorPosition,
|
||||
keyboardTranslateY,
|
||||
inputAccessoryViewAnimatedHeight,
|
||||
isInputAccessoryViewMode,
|
||||
isTransitioningFromCustomView,
|
||||
bottomInset,
|
||||
scrollOffset,
|
||||
keyboardHeight,
|
||||
lastKeyboardHeight,
|
||||
]);
|
||||
|
||||
const handleAndroidKeyboardHide = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -77,10 +77,14 @@ export default function EmojiQuickAction({
|
|||
};
|
||||
|
||||
checkKeyboard();
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastKeyboardHeight, showEmojiPicker]);
|
||||
}, [
|
||||
inputAccessoryViewAnimatedHeight,
|
||||
isInputAccessoryViewMode,
|
||||
isKeyboardFullyClosed,
|
||||
keyboardHeight,
|
||||
lastKeyboardHeight,
|
||||
showEmojiPicker,
|
||||
]);
|
||||
|
||||
const handleButtonPress = usePreventDoubleTap(useCallback(() => {
|
||||
// Prevent opening if already showing or transitioning
|
||||
|
|
@ -129,10 +133,18 @@ export default function EmojiQuickAction({
|
|||
// Dismiss keyboard
|
||||
runOnJS(dismissKeyboard)();
|
||||
})();
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [disabled, showInputAccessoryView, lastKeyboardHeight, setShowInputAccessoryView, scheduleKeyboardCheck, preserveCursorPositionForEmojiPicker]));
|
||||
}, [
|
||||
disabled,
|
||||
showInputAccessoryView,
|
||||
isTransitioningFromCustomView,
|
||||
preserveCursorPositionForEmojiPicker,
|
||||
scheduleKeyboardCheck,
|
||||
keyboardHeight,
|
||||
lastKeyboardHeight,
|
||||
isInputAccessoryViewMode,
|
||||
inputAccessoryViewAnimatedHeight,
|
||||
setShowInputAccessoryView,
|
||||
]));
|
||||
|
||||
const actionTestID = disabled ? `${testID}.disabled` : testID;
|
||||
const color = disabled ? changeOpacity(theme.centerChannelColor, 0.16) : changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {InteractionManager, View} from 'react-native';
|
||||
import Tooltip from 'react-native-walkthrough-tooltip';
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ import CompassIcon from '@components/compass_icon';
|
|||
import ScheduledPostTooltip from '@components/post_draft/send_button/scheduled_post_tooltip';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ const SendButton: React.FC<Props> = ({
|
|||
|
||||
const [scheduledPostTooltipVisible, setScheduledPostTooltipVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (scheduledPostFeatureTooltipWatched || !scheduledPostEnabled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -73,10 +74,7 @@ const SendButton: React.FC<Props> = ({
|
|||
InteractionManager.runAfterInteractions(() => {
|
||||
setScheduledPostTooltipVisible(true);
|
||||
});
|
||||
|
||||
// This effect is intended to run only on the first mount, so dependencies are omitted intentionally.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const onCloseScheduledPostTooltip = useCallback(() => {
|
||||
setScheduledPostTooltipVisible(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {updateDraftFile} from '@actions/local/draft';
|
||||
|
|
@ -9,6 +9,7 @@ import UploadItemShared from '@components/upload_item_shared';
|
|||
import {fileInfoToUploadItemFile} from '@components/upload_item_shared/adapters';
|
||||
import {useEditPost} from '@context/edit_post';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useGalleryItem} from '@hooks/gallery';
|
||||
import DraftEditPostUploadManager from '@managers/draft_upload_manager';
|
||||
|
|
@ -40,7 +41,7 @@ export default function UploadItemWrapper({
|
|||
openGallery(file);
|
||||
}, [openGallery, file]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (file.clientId) {
|
||||
removeCallback.current = DraftEditPostUploadManager.registerProgressHandler(file.clientId, setProgress);
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ export default function UploadItemWrapper({
|
|||
removeCallback.current?.();
|
||||
removeCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (loading && file.clientId) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import TouchableWithFeedback from '@components/touchable_with_feedback';
|
|||
import {Events} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeStyleSheetFromTheme, hexToHue, changeOpacity} from '@utils/theme';
|
||||
|
|
@ -162,49 +163,6 @@ const MoreMessages = ({
|
|||
localUnreadCount.current = unreadCount;
|
||||
}, [unreadCount]);
|
||||
|
||||
const resetCount = async () => {
|
||||
localUnreadCount.current = 0;
|
||||
|
||||
if (resetting.current || (isCRTEnabled && rootId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetting.current = true;
|
||||
await resetMessageCount(serverUrl, channelId);
|
||||
resetting.current = false;
|
||||
};
|
||||
|
||||
const onViewableItemsChanged = (viewableItems: ViewToken[]) => {
|
||||
pressed.current = false;
|
||||
|
||||
if (newMessageLineIndex <= 0 || viewableItems.length === 0 || isManualUnread || resetting.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastViewableIndex = viewableItems.filter((v) => v.isViewable)[viewableItems.length - 1]?.index || 0;
|
||||
const nextViewableIndex = lastViewableIndex + 1;
|
||||
if (viewableItems[0].index === 0 && nextViewableIndex > newMessageLineIndex && !initialScroll.current) {
|
||||
// Auto scroll if the first post is viewable and
|
||||
// * the new message line is viewable OR
|
||||
// * the new message line will be the first next viewable item
|
||||
scrollToIndex(newMessageLineIndex, true, false);
|
||||
resetCount();
|
||||
top.value = 0;
|
||||
initialScroll.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const readCount = posts.slice(0, lastViewableIndex).filter((v) => v.type === 'post').length;
|
||||
const totalUnread = localUnreadCount.current - readCount;
|
||||
if (lastViewableIndex >= newMessageLineIndex) {
|
||||
resetCount();
|
||||
top.value = 0;
|
||||
} else if (totalUnread > 0) {
|
||||
setRemaining(totalUnread);
|
||||
top.value = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const onScrollEndIndex = () => {
|
||||
pressed.current = false;
|
||||
};
|
||||
|
|
@ -239,17 +197,60 @@ const MoreMessages = ({
|
|||
return () => listener.remove();
|
||||
}, [serverUrl, channelId]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const unregister = registerScrollEndIndexListener(onScrollEndIndex);
|
||||
|
||||
return () => unregister();
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function resetCount() {
|
||||
localUnreadCount.current = 0;
|
||||
|
||||
if (resetting.current || (isCRTEnabled && rootId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetting.current = true;
|
||||
await resetMessageCount(serverUrl, channelId);
|
||||
resetting.current = false;
|
||||
}
|
||||
|
||||
function onViewableItemsChanged(viewableItems: ViewToken[]) {
|
||||
pressed.current = false;
|
||||
|
||||
if (newMessageLineIndex <= 0 || viewableItems.length === 0 || isManualUnread || resetting.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastViewableIndex = viewableItems.filter((v) => v.isViewable)[viewableItems.length - 1]?.index || 0;
|
||||
const nextViewableIndex = lastViewableIndex + 1;
|
||||
if (viewableItems[0].index === 0 && nextViewableIndex > newMessageLineIndex && !initialScroll.current) {
|
||||
// Auto scroll if the first post is viewable and
|
||||
// * the new message line is viewable OR
|
||||
// * the new message line will be the first next viewable item
|
||||
scrollToIndex(newMessageLineIndex, true, false);
|
||||
resetCount();
|
||||
top.value = 0;
|
||||
initialScroll.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const readCount = posts.slice(0, lastViewableIndex).filter((v) => v.type === 'post').length;
|
||||
const totalUnread = localUnreadCount.current - readCount;
|
||||
if (lastViewableIndex >= newMessageLineIndex) {
|
||||
resetCount();
|
||||
top.value = 0;
|
||||
} else if (totalUnread > 0) {
|
||||
setRemaining(totalUnread);
|
||||
top.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const unregister = registerViewableItemsListener(onViewableItemsChanged);
|
||||
|
||||
return () => unregister();
|
||||
}, [channelId, unreadCount, newMessageLineIndex, posts]);
|
||||
}, [channelId, unreadCount, newMessageLineIndex, posts, registerViewableItemsListener, isCRTEnabled, rootId, serverUrl, isManualUnread, scrollToIndex, top]);
|
||||
|
||||
useEffect(() => {
|
||||
resetting.current = false;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const Acknowledgements = ({currentUserId, currentUserTimezone, hasReactions, loc
|
|||
const style = getStyleSheet(theme);
|
||||
|
||||
const isCurrentAuthor = post.userId === currentUserId;
|
||||
const acknowledgements = post.metadata?.acknowledgements || [];
|
||||
const acknowledgements = useMemo(() => post.metadata?.acknowledgements || [], [post.metadata?.acknowledgements]);
|
||||
|
||||
const acknowledgedAt = useMemo(() => {
|
||||
if (acknowledgements.length > 0) {
|
||||
|
|
@ -92,7 +92,7 @@ const Acknowledgements = ({currentUserId, currentUserTimezone, hasReactions, loc
|
|||
}
|
||||
}
|
||||
return 0;
|
||||
}, [acknowledgements]);
|
||||
}, [acknowledgements, currentUserId]);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
if ((acknowledgedAt && moreThan5minAgo(acknowledgedAt)) || isCurrentAuthor) {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const UsersList = ({channelId, location, users, userAcknowledgements, timezone}:
|
|||
userAcknowledgement={userAcknowledgements[item.id]}
|
||||
timezone={timezone}
|
||||
/>
|
||||
), [channelId, location, timezone]);
|
||||
), [channelId, location, timezone, userAcknowledgements]);
|
||||
|
||||
if (isTablet) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -58,15 +58,15 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re
|
|||
const containerStyle = useMemo(() => {
|
||||
const minWidth = MIN_WIDTH + (digits * DIGIT_WIDTH);
|
||||
return [styles.reaction, (highlight && styles.highlight), {minWidth}];
|
||||
}, [styles.reaction, highlight, digits]);
|
||||
}, [digits, styles, highlight]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
onLongPress(emojiName);
|
||||
}, []);
|
||||
}, [emojiName, onLongPress]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(emojiName, highlight);
|
||||
}, [highlight]);
|
||||
}, [emojiName, highlight, onPress]);
|
||||
|
||||
const fontStyle = useMemo(() => [styles.count, (highlight && styles.countHighlight)], [styles, highlight]);
|
||||
|
||||
|
|
|
|||
|
|
@ -148,12 +148,11 @@ const Header = ({
|
|||
const usernameOverride = ensureString(post.props?.override_username);
|
||||
const intl = useIntl();
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps -- expire_at triggers recomputation when post metadata changes */
|
||||
const showBoRIcon = useMemo(
|
||||
() => isUnrevealedBoRPost(post),
|
||||
[post, post.metadata?.expire_at],
|
||||
);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
// We need to depend on the expire_at directly,
|
||||
// since changes in it may not be reflected in the post object
|
||||
// (it is still the same object reference).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const showBoRIcon = useMemo(() => isUnrevealedBoRPost(post), [post, post.metadata?.expire_at]);
|
||||
const borExpireAt = post.metadata?.expire_at;
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {useKeyboardAnimationContext} from '@context/keyboard_animation';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {isBoRPost, isUnrevealedBoRPost} from '@utils/bor';
|
||||
|
|
@ -273,10 +274,11 @@ const Post = ({
|
|||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Timer only needs to reset when post.id changes, not on other prop updates
|
||||
// Timer only needs to reset when post.id changes, not on other prop updates
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [post.id]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (!isLastPost) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -287,9 +289,7 @@ const Post = ({
|
|||
|
||||
PerformanceMetricsManager.finishLoad(location === 'Thread' ? 'THREAD' : 'CHANNEL', serverUrl);
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Performance metrics should only run once on mount
|
||||
}, []);
|
||||
});
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setLayoutWidth(e.nativeEvent.layout.width);
|
||||
|
|
|
|||
|
|
@ -183,10 +183,7 @@ const PostList = ({
|
|||
listRef?.current?.scrollToOffset({offset: targetOffset, animated: true});
|
||||
|
||||
setShowScrollToEndBtn(false);
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [listRef]);
|
||||
}, [inputAccessoryViewAnimatedHeight, keyboardHeight, listRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native';
|
||||
|
||||
import {fetchStatusInBatch} from '@actions/remote/user';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Image from './image';
|
||||
|
|
@ -75,11 +76,11 @@ const ProfilePicture = ({
|
|||
const style = getStyleSheet(theme);
|
||||
const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (!isBot && author && !author.status && showStatus) {
|
||||
fetchStatusInBatch(serverUrl, author.id);
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
const viewStyle = useMemo(
|
||||
() => [style.container, {width: size, height: size}, containerStyle],
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const Status = ({author, statusSize, statusStyle, theme}: Props) => {
|
|||
styles.statusWrapper,
|
||||
statusStyle,
|
||||
{borderRadius: statusSize / 2},
|
||||
]), [statusStyle, styles]);
|
||||
]), [statusSize, statusStyle, styles]);
|
||||
const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot);
|
||||
if (author?.status && !isBot) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -108,11 +108,14 @@ const ProgressBar = ({color, containerStyle, progress, withCursor, style, onSeek
|
|||
|
||||
useEffect(() => {
|
||||
progressValue.value = progress;
|
||||
|
||||
// Update the shared value only when the progress changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
widthValue.value = e.nativeEvent.layout.width;
|
||||
}, []);
|
||||
}, [widthValue]);
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={composedGestures}>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import React from 'react';
|
||||
import {type StyleProp, Text, type TextStyle} from 'react-native';
|
||||
|
||||
import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import GroupModel from '@database/models/server/group';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
|
|
@ -33,15 +34,12 @@ const AtMention = ({
|
|||
const group = useMemoMentionedGroup(groups, user, mentionName);
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
// Fetches and updates the local db store with the mention
|
||||
if (!user?.username && !group?.name) {
|
||||
fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName);
|
||||
}
|
||||
|
||||
// Only fetch the user or group on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
let mention;
|
||||
|
||||
|
|
|
|||
|
|
@ -84,21 +84,24 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
|
|||
const searchCancelButtonTestID = `${props.testID}.search.cancel.button`;
|
||||
const searchInputTestID = `${props.testID}.search.input`;
|
||||
|
||||
const onCancelProp = props.onCancel;
|
||||
const onClearProp = props.onClear;
|
||||
const onChangeTextProp = props.onChangeText;
|
||||
const onCancel = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
setValue('');
|
||||
props.onCancel?.();
|
||||
}, [props.onCancel]);
|
||||
onCancelProp?.();
|
||||
}, [onCancelProp]);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setValue('');
|
||||
props.onClear?.();
|
||||
}, [props.onClear]);
|
||||
onClearProp?.();
|
||||
}, [onClearProp]);
|
||||
|
||||
const onChangeText = useCallback((text: string) => {
|
||||
setValue(text);
|
||||
props.onChangeText?.(text);
|
||||
}, [props.onChangeText]);
|
||||
onChangeTextProp?.(text);
|
||||
}, [onChangeTextProp]);
|
||||
|
||||
const cancelButtonProps = useMemo(() => ({
|
||||
buttonTextStyle: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|||
|
||||
import UserList from '@components/user_list';
|
||||
import {General} from '@constants';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {useDebounce} from '@hooks/utils';
|
||||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
|
|
@ -96,16 +97,13 @@ export default function ServerUserList({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [term]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
mounted.current = true;
|
||||
getProfiles();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
|
||||
// We only want to get the profiles on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (isSearch) {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ const ServerVersion = ({isAdmin, lastChecked, version}: ServerVersionProps) => {
|
|||
// Only display the Alert if the TOS does not need to show first
|
||||
handleUnsupportedServer(serverUrl, isAdmin, intl);
|
||||
}
|
||||
|
||||
// We don't care about `intl` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [version, isAdmin, lastChecked, serverUrl]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function TextSetting({
|
|||
const style = getStyleSheet(theme);
|
||||
|
||||
const inputContainerStyle = useMemo(() => (disabled ? [style.inputContainer, style.disabled] : style.inputContainer), [style, disabled]);
|
||||
const inputStyle = useMemo(() => (multiline ? style.multiline : style.input), [multiline]);
|
||||
const inputStyle = useMemo(() => (multiline ? style.multiline : style.input), [multiline, style]);
|
||||
|
||||
const actualKeyboardType: KeyboardTypeOptions = keyboardType === 'url' ? Platform.select({android: 'default', default: 'url'}) : keyboardType;
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const Highlighter = ({code, language, textStyle, selectable = false}: SyntaxHigl
|
|||
selectable={selectable}
|
||||
/>
|
||||
);
|
||||
}, [textStyle, theme, style]);
|
||||
}, [style.hljs.color, theme.centerChannelColor, textStyle, selectable]);
|
||||
|
||||
const preTag = useCallback((info: any) => (
|
||||
<View
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ const CodeHighlightRenderer = ({defaultColor, digits, fontFamily, fontSize, rows
|
|||
selectable,
|
||||
digits,
|
||||
});
|
||||
}, [defaultColor, digits, fontFamily, fontSize, stylesheet]);
|
||||
}, [defaultColor, digits, fontFamily, fontSize, selectable, stylesheet]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default function TeamItem({team, hasUnreads, mentionCount, selected}: Pro
|
|||
|
||||
PerformanceMetricsManager.startMetric('mobile_team_switch');
|
||||
handleTeamChange(serverUrl, team.id);
|
||||
}, [selected, team?.id, serverUrl]);
|
||||
}, [team, selected, serverUrl]);
|
||||
|
||||
if (!team) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -55,10 +55,16 @@ export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneT
|
|||
|
||||
useEffect(() => {
|
||||
marginTop.value = iconPad ? 44 : 0;
|
||||
|
||||
// Update the shared value only when the iconPad changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [iconPad]);
|
||||
|
||||
useEffect(() => {
|
||||
width.value = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
|
||||
// Update the shared value only when the hasMoreThanOneTeam changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasMoreThanOneTeam]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const Toast = ({animatedStyle, children, style, iconName, message, textStyle, te
|
|||
const toast_width = isTablet ? WIDTH_TABLET : WIDTH_MOBILE;
|
||||
const width = Math.min(dim.height, dim.width, toast_width) - TOAST_MARGIN;
|
||||
return [styles.container, {width}, style];
|
||||
}, [dim, styles.container, style]);
|
||||
}, [isTablet, dim, styles, style]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, o
|
|||
if (onShow) {
|
||||
setTimeout(onShow, 1000);
|
||||
}
|
||||
}, [itemBounds]);
|
||||
}, [onShow]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, d
|
|||
}
|
||||
|
||||
callback(...args);
|
||||
|
||||
// The dependencies should be passed by the caller.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [keyboardContext, ...dependencies]));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useLayoutEffect} from 'react';
|
||||
import {useLayoutEffect} from 'react';
|
||||
import {makeMutable, runOnUI, type AnimatedRef, type SharedValue} from 'react-native-reanimated';
|
||||
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
import type {GalleryManagerSharedValues} from '@typings/screens/gallery';
|
||||
|
||||
export interface GalleryManagerItem {
|
||||
|
|
@ -155,13 +157,16 @@ export function GalleryInit({children, galleryIdentifier}: GalleryInitProps) {
|
|||
return () => {
|
||||
gallery.reset();
|
||||
};
|
||||
|
||||
// Execute only on mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
return () => {
|
||||
galleryManager.remove(galleryIdentifier);
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
return children;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,20 +161,26 @@ export const useKeyboardAnimationContext = () => {
|
|||
updateValue: defaultUpdateValue.current,
|
||||
updateCursorPosition: defaultUpdateCursorPosition.current,
|
||||
registerPostInputCallbacks: defaultRegisterPostInputCallbacks,
|
||||
|
||||
// Shared values don't need to be in dependencies - they're stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}), [
|
||||
defaultKeyboardTranslateY,
|
||||
defaultBottomInset,
|
||||
defaultScrollOffset,
|
||||
defaultKeyboardHeight,
|
||||
defaultScrollPosition,
|
||||
defaultOnScroll,
|
||||
defaultInputRef,
|
||||
defaultBlurInput,
|
||||
defaultFocusInput,
|
||||
defaultBlurAndDismissKeyboard,
|
||||
defaultIsKeyboardFullyOpen,
|
||||
defaultIsKeyboardFullyClosed,
|
||||
defaultIsKeyboardInTransition,
|
||||
defaultSetShowInputAccessoryView,
|
||||
defaultIsInputAccessoryViewMode,
|
||||
defaultInputAccessoryViewAnimatedHeight,
|
||||
defaultIsTransitioningFromCustomView,
|
||||
defaultCloseInputAccessoryView,
|
||||
defaultScrollToEnd,
|
||||
defaultSetIsEmojiSearchFocused,
|
||||
defaultCursorPositionRef,
|
||||
defaultRegisterCursorPosition,
|
||||
defaultPreserveCursorPositionForEmojiPicker,
|
||||
defaultClearCursorPositionPreservation,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ export function useViewPosition(viewRef: RefObject<View>, deps: React.Dependency
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The deps should be passed by the caller.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps, isTablet, height, viewRef, modalPosition]);
|
||||
|
||||
return modalPosition;
|
||||
|
|
|
|||
55
app/hooks/did_mount.test.ts
Normal file
55
app/hooks/did_mount.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import useDidMount from './did_mount';
|
||||
|
||||
describe('useDidMount', () => {
|
||||
test('should call callback on mount', () => {
|
||||
const callback = jest.fn();
|
||||
renderHook(() => useDidMount(callback));
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not call callback on re-renders', () => {
|
||||
const callback = jest.fn();
|
||||
const {rerender} = renderHook(() => useDidMount(callback));
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call cleanup function on unmount when callback returns one', () => {
|
||||
const cleanup = jest.fn();
|
||||
const callback = jest.fn(() => cleanup);
|
||||
const {unmount} = renderHook(() => useDidMount(callback));
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(cleanup).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(cleanup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not throw when callback returns undefined', () => {
|
||||
const callback = jest.fn(() => undefined);
|
||||
expect(() => {
|
||||
renderHook(() => useDidMount(callback));
|
||||
}).not.toThrow();
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
12
app/hooks/did_mount.ts
Normal file
12
app/hooks/did_mount.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, type EffectCallback} from 'react';
|
||||
|
||||
function useDidMount(callback: EffectCallback) {
|
||||
// We only want to run this effect on mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(callback, []);
|
||||
}
|
||||
|
||||
export default useDidMount;
|
||||
|
|
@ -12,6 +12,9 @@ function useDidUpdate(callback: EffectCallback, deps?: DependencyList) {
|
|||
} else {
|
||||
hasMount.current = true;
|
||||
}
|
||||
|
||||
// The dependencies should be provided by the caller.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {subject} from '@store/fetching_thread_store';
|
||||
|
||||
import useDidMount from './did_mount';
|
||||
|
||||
export const useFetchingThreadState = (rootId: string) => {
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const sub = subject.pipe(
|
||||
switchMap((s) => of$(s[rootId] || false)),
|
||||
distinctUntilChanged(),
|
||||
).subscribe(setIsFetching);
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
});
|
||||
|
||||
return isFetching;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {useCallback} from 'react';
|
||||
import {
|
||||
Easing, runOnJS, useAnimatedRef, useAnimatedStyle,
|
||||
useSharedValue,
|
||||
|
|
@ -10,6 +10,8 @@ import {
|
|||
|
||||
import {useGallery} from '@context/gallery';
|
||||
|
||||
import useDidMount from './did_mount';
|
||||
|
||||
export function diff(context: any, name: string, value: any) {
|
||||
'worklet';
|
||||
|
||||
|
|
@ -105,12 +107,9 @@ export function useGalleryItem(
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
gallery.registerItem(index, ref);
|
||||
|
||||
// Only register item once on mount - gallery, index, and ref are stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const onGestureEvent = () => {
|
||||
'worklet';
|
||||
|
|
|
|||
|
|
@ -153,12 +153,12 @@ export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset:
|
|||
// No scroll for section lists?
|
||||
}
|
||||
}
|
||||
}, [largeHeight, defaultHeight]);
|
||||
}, [headerOffset, animatedRef, scrollValue, insets.top, scrollEnabled, defaultHeight, autoScroll]);
|
||||
|
||||
const unlock = useCallback(() => {
|
||||
scrollEnabled.value = true;
|
||||
setLockValue(0);
|
||||
}, []);
|
||||
}, [scrollEnabled]);
|
||||
|
||||
return {
|
||||
defaultHeight,
|
||||
|
|
|
|||
58
app/hooks/initial_value.test.ts
Normal file
58
app/hooks/initial_value.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import useInitialValue from './initial_value';
|
||||
|
||||
describe('useInitialValue', () => {
|
||||
test('should return the value from the factory on first render', () => {
|
||||
const factory = jest.fn(() => 42);
|
||||
const {result} = renderHook(() => useInitialValue(factory));
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(42);
|
||||
});
|
||||
|
||||
test('should call factory only once and keep same value on re-renders', () => {
|
||||
let callCount = 0;
|
||||
const factory = jest.fn(() => {
|
||||
callCount += 1;
|
||||
return callCount;
|
||||
});
|
||||
const {result, rerender} = renderHook(() => useInitialValue(factory));
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(1);
|
||||
|
||||
act(() => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(1);
|
||||
|
||||
act(() => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(1);
|
||||
});
|
||||
|
||||
test('should work with object values', () => {
|
||||
const initial = {id: 1, name: 'test'};
|
||||
const factory = jest.fn(() => initial);
|
||||
const {result} = renderHook(() => useInitialValue(factory));
|
||||
|
||||
expect(result.current).toEqual(initial);
|
||||
expect(result.current).toBe(initial);
|
||||
});
|
||||
|
||||
test('should work with string values', () => {
|
||||
const factory = jest.fn(() => 'hello');
|
||||
const {result} = renderHook(() => useInitialValue(factory));
|
||||
|
||||
expect(result.current).toBe('hello');
|
||||
});
|
||||
});
|
||||
12
app/hooks/initial_value.ts
Normal file
12
app/hooks/initial_value.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useMemo} from 'react';
|
||||
|
||||
function useInitialValue<T>(factory: () => T) {
|
||||
// We only want the initial value, no updates.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useMemo<T>(factory, []);
|
||||
}
|
||||
|
||||
export default useInitialValue;
|
||||
|
|
@ -18,6 +18,9 @@ const useNavButtonPressed = (navButtonId: string, componentId: string, callback:
|
|||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
|
||||
// The dependencies should be passed by the caller.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
};
|
||||
|
||||
|
|
|
|||
94
app/hooks/notification_props.test.ts
Normal file
94
app/hooks/notification_props.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {getNotificationProps} from '@utils/user';
|
||||
|
||||
import useNotificationProps from './notification_props';
|
||||
|
||||
describe('useNotificationProps', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return result of getNotificationProps when given a user', () => {
|
||||
const notifyProps = TestHelper.fakeUserNotifyProps({channel: 'true', push: 'all'});
|
||||
const user = TestHelper.fakeUserModel({notifyProps});
|
||||
const expectedProps = notifyProps;
|
||||
|
||||
const {result} = renderHook(() => useNotificationProps(user));
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
});
|
||||
|
||||
test('should call getNotificationProps with undefined when currentUser is undefined', () => {
|
||||
const defaultProps = getNotificationProps(undefined);
|
||||
|
||||
const {result} = renderHook(() => useNotificationProps(undefined));
|
||||
|
||||
expect(result.current).toEqual(defaultProps);
|
||||
});
|
||||
|
||||
test('should memoize based on notifyProps and not recompute when user reference changes but notifyProps is same', () => {
|
||||
const notifyProps = TestHelper.fakeUserNotifyProps({channel: 'false'});
|
||||
const user1 = TestHelper.fakeUserModel({notifyProps});
|
||||
const user2 = TestHelper.fakeUserModel({...user1, id: 'other-id'});
|
||||
user2.notifyProps = notifyProps;
|
||||
const expectedProps = notifyProps;
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useNotificationProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
|
||||
act(() => {
|
||||
rerender({currentUser: user2});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
});
|
||||
|
||||
test('should recompute when notifyProps changes', () => {
|
||||
const notifyProps1 = TestHelper.fakeUserNotifyProps({channel: 'false'});
|
||||
const notifyProps2 = TestHelper.fakeUserNotifyProps({channel: 'true'});
|
||||
const user1 = TestHelper.fakeUserModel({notifyProps: notifyProps1});
|
||||
const user2 = TestHelper.fakeUserModel({...user1, notifyProps: notifyProps2});
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useNotificationProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(notifyProps1);
|
||||
|
||||
act(() => {
|
||||
rerender({currentUser: user2});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(notifyProps2);
|
||||
});
|
||||
|
||||
test('should recompute when notifyProps changes even if user reference doesnt change', () => {
|
||||
const notifyProps1 = TestHelper.fakeUserNotifyProps({channel: 'false'});
|
||||
const notifyProps2 = TestHelper.fakeUserNotifyProps({channel: 'true'});
|
||||
const user1 = TestHelper.fakeUserModel({notifyProps: notifyProps1});
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useNotificationProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(notifyProps1);
|
||||
|
||||
user1.notifyProps = notifyProps2;
|
||||
act(() => {
|
||||
rerender({currentUser: user1});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(notifyProps2);
|
||||
});
|
||||
});
|
||||
18
app/hooks/notification_props.ts
Normal file
18
app/hooks/notification_props.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useMemo} from 'react';
|
||||
|
||||
import {getNotificationProps} from '@utils/user';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
function useNotificationProps(currentUser?: UserModel) {
|
||||
// We need to depend on the notifyProps object directly,
|
||||
// since changes in it may not be reflected in the currentUser object
|
||||
// (it is still the same object reference).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useMemo(() => getNotificationProps(currentUser), [currentUser?.notifyProps]);
|
||||
}
|
||||
|
||||
export default useNotificationProps;
|
||||
|
|
@ -1,24 +1,26 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {getLoadingTeamChannelsSubject} from '@store/team_load_store';
|
||||
|
||||
import useDidMount from './did_mount';
|
||||
|
||||
export const useTeamsLoading = (serverUrl: string) => {
|
||||
// const subject = getLoadingTeamChannelsSubject(serverUrl);
|
||||
// const [loading, setLoading] = useState(subject.getValue() !== 0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const sub = getLoadingTeamChannelsSubject(serverUrl).pipe(
|
||||
switchMap((v) => of$(v !== 0)),
|
||||
distinctUntilChanged(),
|
||||
).subscribe(setLoading);
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
});
|
||||
|
||||
return loading;
|
||||
};
|
||||
|
|
|
|||
110
app/hooks/user_timezone.test.tsx
Normal file
110
app/hooks/user_timezone.test.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import useUserTimezoneProps from './user_timezone';
|
||||
|
||||
describe('useUserTimezoneProps', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return result of getUserTimezoneProps when given a user with timezone', () => {
|
||||
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'};
|
||||
const user = TestHelper.fakeUserModel({timezone});
|
||||
const expectedProps = {useAutomaticTimezone: true, automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'};
|
||||
|
||||
const {result} = renderHook(() => useUserTimezoneProps(user));
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
});
|
||||
|
||||
test('should call getUserTimezoneProps with undefined when currentUser is undefined', () => {
|
||||
const defaultProps = {useAutomaticTimezone: true, automaticTimezone: '', manualTimezone: ''};
|
||||
|
||||
const {result} = renderHook(() => useUserTimezoneProps(undefined));
|
||||
|
||||
expect(result.current).toEqual(defaultProps);
|
||||
});
|
||||
|
||||
test('should not recompute on rerender when same user is passed', () => {
|
||||
const timezone = {useAutomaticTimezone: 'false', automaticTimezone: '', manualTimezone: 'Europe/London'};
|
||||
const user = TestHelper.fakeUserModel({timezone});
|
||||
const expectedProps = {useAutomaticTimezone: false, automaticTimezone: '', manualTimezone: 'Europe/London'};
|
||||
|
||||
const {result, rerender} = renderHook(() => useUserTimezoneProps(user));
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
|
||||
act(() => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
});
|
||||
|
||||
test('should recompute when timezone changes', () => {
|
||||
const timezone1 = {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: ''};
|
||||
const timezone2 = {useAutomaticTimezone: 'false', automaticTimezone: '', manualTimezone: 'Europe/London'};
|
||||
const user1 = TestHelper.fakeUserModel({timezone: timezone1});
|
||||
const user2 = TestHelper.fakeUserModel({...user1, timezone: timezone2});
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useUserTimezoneProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({useAutomaticTimezone: true, automaticTimezone: 'America/New_York', manualTimezone: ''});
|
||||
|
||||
act(() => {
|
||||
rerender({currentUser: user2});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({useAutomaticTimezone: false, automaticTimezone: '', manualTimezone: 'Europe/London'});
|
||||
});
|
||||
|
||||
test('should not recompute when user reference changes but timezone is the same', () => {
|
||||
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: 'America/Chicago', manualTimezone: ''};
|
||||
const user1 = TestHelper.fakeUserModel({timezone});
|
||||
const user2 = TestHelper.fakeUserModel({...user1, id: 'other-id'});
|
||||
(user2 as typeof user1).timezone = timezone;
|
||||
const expectedProps = {useAutomaticTimezone: true, automaticTimezone: 'America/Chicago', manualTimezone: ''};
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useUserTimezoneProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
|
||||
act(() => {
|
||||
rerender({currentUser: user2});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(expectedProps);
|
||||
});
|
||||
|
||||
test('should recompute when timezone changes even if user reference doesnt change', () => {
|
||||
const timezone1 = {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: ''};
|
||||
const timezone2 = {useAutomaticTimezone: 'false', automaticTimezone: '', manualTimezone: 'Europe/London'};
|
||||
const user1 = TestHelper.fakeUserModel({timezone: timezone1});
|
||||
|
||||
const {result, rerender} = renderHook(
|
||||
({currentUser}) => useUserTimezoneProps(currentUser),
|
||||
{initialProps: {currentUser: user1}},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({useAutomaticTimezone: true, automaticTimezone: 'America/New_York', manualTimezone: ''});
|
||||
|
||||
user1.timezone = timezone2;
|
||||
|
||||
act(() => {
|
||||
rerender({currentUser: user1});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({useAutomaticTimezone: false, automaticTimezone: '', manualTimezone: 'Europe/London'});
|
||||
});
|
||||
});
|
||||
18
app/hooks/user_timezone.tsx
Normal file
18
app/hooks/user_timezone.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useMemo} from 'react';
|
||||
|
||||
import {getUserTimezoneProps} from '@utils/user';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
function useUserTimezoneProps(currentUser?: UserModel) {
|
||||
// We need to depend on the timezone object directly,
|
||||
// since changes in it may not be reflected in the currentUser object
|
||||
// (it is still the same object reference).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useMemo(() => getUserTimezoneProps(currentUser), [currentUser?.timezone]);
|
||||
}
|
||||
|
||||
export default useUserTimezoneProps;
|
||||
|
|
@ -51,6 +51,10 @@ const CallDuration = ({value, style, truncateWhenLong, updateIntervalInSeconds}:
|
|||
return function cleanup() {
|
||||
return null;
|
||||
};
|
||||
|
||||
// We don't care about `getCallDuration` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getServerDisplayName} from '@queries/app/servers';
|
||||
import ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
|
@ -132,15 +133,19 @@ export const CallNotification = ({
|
|||
const [serverName, setServerName] = useState('');
|
||||
const moreThanOneServer = servers.length > 1;
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
|
||||
if (!channelMembers?.length) {
|
||||
fetchProfilesInChannel(serverUrl, incomingCall.channelID, currentUserId, undefined, false);
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
playIncomingCallsRinging(incomingCall.serverUrl, incomingCall.callID, userStatus || '');
|
||||
|
||||
// We don't care about `userStatus` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [incomingCall.serverUrl, incomingCall.callID, appState]);
|
||||
|
||||
// We only need to getServerDisplayName once
|
||||
|
|
@ -165,7 +170,7 @@ export const CallNotification = ({
|
|||
const onDismissPress = useCallback(() => {
|
||||
removeIncomingCall(serverUrl, incomingCall.callID, incomingCall.channelID);
|
||||
dismissIncomingCall(incomingCall.serverUrl, incomingCall.channelID);
|
||||
}, [incomingCall]);
|
||||
}, [incomingCall, serverUrl]);
|
||||
|
||||
let message: React.ReactElement;
|
||||
if (incomingCall.type === ChannelType.DM) {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export const CallsCustomMessage = ({
|
|||
setJoiningChannelId(post.channelId);
|
||||
await leaveAndJoinWithAlert(intl, serverUrl, post.channelId);
|
||||
setJoiningChannelId(null);
|
||||
}, [limitRestrictedInfo, intl, serverUrl, post.channelId]);
|
||||
}, [isLimitRestricted, post.channelId, intl, serverUrl, limitRestrictedInfo]);
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
leaveCallConfirmation(intl, otherParticipants, isAdmin, isHost, serverUrl, post.channelId);
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
|
|||
if (!micPermissionsGranted) {
|
||||
asyncFn();
|
||||
}
|
||||
|
||||
// We don't care about `micPermissionsGranted` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appState]);
|
||||
|
||||
return hasPermission;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import {useTheme} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {
|
||||
|
|
@ -363,14 +364,10 @@ const CallScreen = ({
|
|||
id: 'mobile.calls_stop_recording',
|
||||
defaultMessage: 'Stop Recording',
|
||||
});
|
||||
const openChannelOptionTitle = intl.formatMessage({
|
||||
id: 'mobile.calls_open_channel',
|
||||
defaultMessage: 'Open Channel',
|
||||
});
|
||||
const showCCTitle = intl.formatMessage({id: 'mobile.calls_show_cc', defaultMessage: 'Show live captions'});
|
||||
const hideCCTitle = intl.formatMessage({id: 'mobile.calls_hide_cc', defaultMessage: 'Hide live captions'});
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const setOrientation = () => {
|
||||
mergeNavigationOptions('Call', {
|
||||
layout: {
|
||||
|
|
@ -418,7 +415,7 @@ const CallScreen = ({
|
|||
|
||||
unsetOrientation();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
leaveCallConfirmation(intl, otherParticipants, isAdmin, isHost, serverUrl, currentCall?.channelId || '', popTopScreen);
|
||||
|
|
@ -448,7 +445,7 @@ const CallScreen = ({
|
|||
}
|
||||
|
||||
await startCallRecording(currentCall.serverUrl, currentCall.channelId);
|
||||
}, [currentCall?.channelId, currentCall?.serverUrl]);
|
||||
}, [currentCall]);
|
||||
|
||||
const stopRecording = useCallback(async () => {
|
||||
const stop = await stopRecordingConfirmationAlert(intl, EnableTranscriptions);
|
||||
|
|
@ -464,7 +461,7 @@ const CallScreen = ({
|
|||
}
|
||||
|
||||
await stopCallRecording(currentCall.serverUrl, currentCall.channelId);
|
||||
}, [currentCall?.channelId, currentCall?.serverUrl, EnableTranscriptions]);
|
||||
}, [intl, EnableTranscriptions, currentCall]);
|
||||
|
||||
const toggleCC = useCallback(async () => {
|
||||
Keyboard.dismiss();
|
||||
|
|
@ -495,7 +492,7 @@ const CallScreen = ({
|
|||
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
|
||||
WebsocketManager.initializeClient(currentCall.serverUrl, 'Server Switch');
|
||||
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
|
||||
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);
|
||||
}, [currentCall, componentId, fromThreadScreen, callThreadOptionTitle]);
|
||||
|
||||
// The user should receive a recording alert if all of the following conditions apply:
|
||||
// - Recording has started, recording has not ended
|
||||
|
|
@ -581,10 +578,26 @@ const CallScreen = ({
|
|||
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
|
||||
theme,
|
||||
});
|
||||
}, [intl, theme, isHost, EnableRecordings, waitingForRecording, recording, startRecording,
|
||||
recordOptionTitle, stopRecording, stopRecordingOptionTitle, style, switchToThread,
|
||||
callThreadOptionTitle, openChannelOptionTitle, ccAvailable, toggleCC, showCC, hideCCTitle,
|
||||
showCCTitle]);
|
||||
}, [
|
||||
isHost,
|
||||
EnableRecordings,
|
||||
ccAvailable,
|
||||
intl,
|
||||
theme,
|
||||
showStartRecording,
|
||||
startRecording,
|
||||
recordOptionTitle,
|
||||
showStopRecording,
|
||||
style,
|
||||
stopRecording,
|
||||
stopRecordingOptionTitle,
|
||||
switchToThread,
|
||||
callThreadOptionTitle,
|
||||
toggleCC,
|
||||
showCC,
|
||||
hideCCTitle,
|
||||
showCCTitle,
|
||||
]);
|
||||
|
||||
const collapse = useCallback(() => {
|
||||
popTopScreen(componentId);
|
||||
|
|
@ -611,6 +624,10 @@ const CallScreen = ({
|
|||
} else {
|
||||
setCenterUsers(true);
|
||||
}
|
||||
|
||||
// We don't care about `avatarSize`, `screenShareOn`, and `smallerAvatar` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layout, numSessions]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {type CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfigState>> = {};
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ export const useCallsConfig = (serverUrl: string) => {
|
|||
|
||||
const callsConfigSubject = getCallsConfigSubject(serverUrl);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const subscription = callsConfigSubject.subscribe((callsConfig) => {
|
||||
setState(callsConfig);
|
||||
});
|
||||
|
|
@ -41,7 +42,7 @@ export const useCallsConfig = (serverUrl: string) => {
|
|||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {type CallsState, DefaultCallsState} from '@calls/types/calls';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
const callsStateSubjects: Dictionary<BehaviorSubject<CallsState>> = {};
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ export const useCallsState = (serverUrl: string) => {
|
|||
|
||||
const callsStateSubject = getCallsStateSubject(serverUrl);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const subscription = callsStateSubject.subscribe((callsState) => {
|
||||
setState(callsState);
|
||||
});
|
||||
|
|
@ -41,7 +42,7 @@ export const useCallsState = (serverUrl: string) => {
|
|||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
import type {ChannelsWithCalls} from '@calls/types/calls';
|
||||
|
||||
const channelsWithCallsSubject: Dictionary<BehaviorSubject<ChannelsWithCalls>> = {};
|
||||
|
|
@ -31,7 +33,7 @@ export const observeChannelsWithCalls = (serverUrl: string) => {
|
|||
export const useChannelsWithCalls = (serverUrl: string) => {
|
||||
const [state, setState] = useState<ChannelsWithCalls>({});
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const subscription = getChannelsWithCallsSubject(serverUrl).subscribe((channelsWithCalls) => {
|
||||
setState(channelsWithCalls);
|
||||
});
|
||||
|
|
@ -39,7 +41,7 @@ export const useChannelsWithCalls = (serverUrl: string) => {
|
|||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import {fetchUsersByIds} from '@actions/remote/user';
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import Markdown from '@components/markdown';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {isPostStatusUpdateProps} from '@playbooks/utils/types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -87,14 +88,11 @@ const StatusUpdatePost = ({location, post, theme}: Props) => {
|
|||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (statusUpdateProps) {
|
||||
fetchUsersByIds(serverUrl, statusUpdateProps.participantIds);
|
||||
}
|
||||
|
||||
// Only do this on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
if (!statusUpdateProps) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {fetchPlaybookRunsPageForParticipant} from '@playbooks/actions/remote/runs';
|
||||
import RunList from '@playbooks/components/run_list';
|
||||
import {isRunFinished} from '@playbooks/utils/run';
|
||||
|
|
@ -86,12 +87,9 @@ const ParticipantPlaybooks = ({
|
|||
}
|
||||
}, [loadingMore, hasMore, currentPage, fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
fetchData();
|
||||
|
||||
// Only fetch the data on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const showMoreButton = useCallback(() => {
|
||||
return hasMore;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAvoidKeyboard} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from '@playbooks/actions/remote/runs';
|
||||
|
|
@ -121,7 +122,7 @@ const PostUpdate = ({
|
|||
});
|
||||
}, [rightButton, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
async function initialLoad() {
|
||||
let calculatedFollowersCount = 0;
|
||||
let calculatedBroadcastChannelCount = 0;
|
||||
|
|
@ -161,10 +162,7 @@ const PostUpdate = ({
|
|||
}
|
||||
|
||||
initialLoad();
|
||||
|
||||
// This is the initial load, so we don't need to re-run it on every change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const onNextUpdateSelected = useCallback((value: SelectedDialogOption) => {
|
||||
if (!value) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {General, Screens} from '@constants';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {fetchPlaybooks} from '@playbooks/actions/remote/playbooks';
|
||||
import {fetchPlaybookRunsForChannel} from '@playbooks/actions/remote/runs';
|
||||
|
|
@ -185,12 +186,9 @@ function SelectPlaybook({
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
loadMore();
|
||||
|
||||
// We only want to load the playbooks once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const renderNoResults = useCallback((): JSX.Element | null => {
|
||||
if (loading && page.current === -1) {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export default function BrowseChannels(props: Props) {
|
|||
}
|
||||
|
||||
setButtons(componentId, buttons);
|
||||
}, [closeButton, canCreateChannels, intl.locale, theme, componentId]);
|
||||
}, [closeButton, canCreateChannels, componentId, theme, intl]);
|
||||
|
||||
const onSelectChannel = useCallback(async (channel: Channel) => {
|
||||
setHeaderButtons(false);
|
||||
|
|
@ -157,7 +157,7 @@ export default function BrowseChannels(props: Props) {
|
|||
switchToChannelById(serverUrl, channel.id, currentTeamId);
|
||||
close();
|
||||
}
|
||||
}, [setHeaderButtons, intl.locale]);
|
||||
}, [setHeaderButtons, serverUrl, currentTeamId, intl]);
|
||||
|
||||
const onSearch = useCallback(() => {
|
||||
searchChannels(term);
|
||||
|
|
@ -167,7 +167,7 @@ export default function BrowseChannels(props: Props) {
|
|||
const screen = Screens.CREATE_OR_EDIT_CHANNEL;
|
||||
const title = intl.formatMessage({id: 'mobile.create_channel.title', defaultMessage: 'New channel'});
|
||||
goToScreen(screen, title);
|
||||
}, [intl.locale]);
|
||||
}, [intl]);
|
||||
|
||||
useNavButtonPressed(CLOSE_BUTTON_ID, componentId, close, [close]);
|
||||
useNavButtonPressed(CREATE_BUTTON_ID, componentId, handleCreate, [handleCreate]);
|
||||
|
|
@ -176,7 +176,7 @@ export default function BrowseChannels(props: Props) {
|
|||
useEffect(() => {
|
||||
// Update header buttons in case anything related to the header changes
|
||||
setHeaderButtons(!adding);
|
||||
}, [theme, canCreateChannels, adding]);
|
||||
}, [adding, setHeaderButtons]);
|
||||
|
||||
let content;
|
||||
if (adding) {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export default function ChannelList({
|
|||
);
|
||||
|
||||
//Style is covered by the theme
|
||||
}, [loading, theme]);
|
||||
}, [loading, style, theme.buttonBg]);
|
||||
|
||||
const renderNoResults = useCallback(() => {
|
||||
if (term) {
|
||||
|
|
@ -119,7 +119,7 @@ export default function ChannelList({
|
|||
<View
|
||||
style={style.separator}
|
||||
/>
|
||||
), [theme]);
|
||||
), [style]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export default function SearchHandler(props: Props) {
|
|||
|
||||
const isSearch = Boolean(term);
|
||||
|
||||
const doGetChannels = (t: string) => {
|
||||
const doGetChannels = useCallback((t: string) => {
|
||||
let next: (typeof nextPublic | typeof nextShared | typeof nextArchived);
|
||||
let fetch: (typeof fetchChannels | typeof fetchSharedChannels | typeof fetchArchivedChannels);
|
||||
let page: (typeof publicPage | typeof sharedPage | typeof archivedPage);
|
||||
|
|
@ -187,13 +187,13 @@ export default function SearchHandler(props: Props) {
|
|||
() => dispatch(StopAction),
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [currentTeamId, serverUrl]);
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
if (!loading && !term) {
|
||||
doGetChannels(typeOfChannels);
|
||||
}
|
||||
}, [typeOfChannels, loading, term]);
|
||||
}, [loading, term, doGetChannels, typeOfChannels]);
|
||||
|
||||
let activeChannels: Channel[];
|
||||
switch (typeOfChannels) {
|
||||
|
|
@ -210,7 +210,7 @@ export default function SearchHandler(props: Props) {
|
|||
const stopSearch = useCallback(() => {
|
||||
setSearchResults(defaultSearchResults);
|
||||
setTerm('');
|
||||
}, [activeChannels]);
|
||||
}, []);
|
||||
|
||||
const doSearchChannels = useCallback((text: string) => {
|
||||
if (text) {
|
||||
|
|
@ -231,7 +231,7 @@ export default function SearchHandler(props: Props) {
|
|||
} else {
|
||||
stopSearch();
|
||||
}
|
||||
}, [activeChannels, visibleChannels, joinedChannels, stopSearch]);
|
||||
}, [searchResults, serverUrl, currentTeamId, stopSearch]);
|
||||
|
||||
const changeChannelType = useCallback((channelType: string) => {
|
||||
setTypeOfChannels(channelType);
|
||||
|
|
@ -276,12 +276,20 @@ export default function SearchHandler(props: Props) {
|
|||
return () => {
|
||||
loadedChannels.current = async () => {/* Do nothing */};
|
||||
};
|
||||
|
||||
// We don't care about `doGetChannels` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [joinedChannels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSearch) {
|
||||
doGetChannels(typeOfChannels);
|
||||
}
|
||||
|
||||
// We don't care about `doGetChannels` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typeOfChannels, isSearch]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import PostList from '@components/post_list';
|
|||
import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useAppState, useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useDebounce} from '@hooks/utils';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
|
@ -106,14 +107,11 @@ const ChannelPostList = ({
|
|||
}
|
||||
}, [appState === 'active']);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
return () => {
|
||||
unsetActiveChannelOnServer(serverUrl);
|
||||
};
|
||||
|
||||
// We only want to run this on unmount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
const intro = (<Intro channelId={channelId}/>);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {defineMessages} from 'react-intl';
|
||||
import {Text, View, type TextStyle} from 'react-native';
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ import FormattedText from '@components/formatted_text';
|
|||
import {BotTag} from '@components/tag';
|
||||
import {General, NotificationLevel} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
|
@ -124,12 +125,12 @@ const DirectChannel = ({
|
|||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
|
||||
if (!channelMembers?.length) {
|
||||
fetchProfilesInChannel(serverUrl, channel.id, currentUserId, undefined, false);
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import {fetchChannelCreator} from '@actions/remote/channel';
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import {General, Permissions} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {hasPermission} from '@utils/role';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -93,11 +94,11 @@ const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
|
|||
return <PrivateChannel theme={theme}/>;
|
||||
}, [channel.type, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (!creator && channel.creatorId) {
|
||||
fetchChannelCreator(serverUrl, channel.id);
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
const canManagePeople = useMemo(() => {
|
||||
if (channel.deleteAt !== 0) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
|
|
@ -9,6 +8,7 @@ import {savePreference} from '@actions/remote/preference';
|
|||
import {Preferences} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
|
@ -17,7 +17,7 @@ const useGMasDMNotice = (userId: string, channelType: ChannelType, dismissedGMas
|
|||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (!hasGMasDMFeature) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ const useGMasDMNotice = (userId: string, channelType: ChannelType, dismissedGMas
|
|||
},
|
||||
],
|
||||
);
|
||||
}, []);
|
||||
});
|
||||
};
|
||||
|
||||
export default useGMasDMNotice;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import TouchableWithFeedback from '@components/touchable_with_feedback';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {fileSizeWarning, getExtensionFromMime, getFormattedFileSize} from '@utils/file';
|
||||
import PickerUtil from '@utils/file/file_picker';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -130,7 +131,7 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
const [uploading, setUploading] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
const subContainerStyle = useMemo(() => [styles.viewContainer, {paddingHorizontal: isTablet ? 42 : 0}], [isTablet]);
|
||||
const subContainerStyle = useMemo(() => [styles.viewContainer, {paddingHorizontal: isTablet ? 42 : 0}], [isTablet, styles]);
|
||||
const cancelUpload = useRef<() => void | undefined>();
|
||||
|
||||
const onProgress = useCallback((p: number, bytes: number) => {
|
||||
|
|
@ -143,7 +144,18 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
|
||||
setProgress(p);
|
||||
setFile(f);
|
||||
}, []);
|
||||
}, [file]);
|
||||
|
||||
const setUploadError = useCallback(() => {
|
||||
setProgress(0);
|
||||
setUploading(false);
|
||||
setFailed(true);
|
||||
|
||||
setError(intl.formatMessage({
|
||||
id: 'channel_bookmark.add.file_upload_error',
|
||||
defaultMessage: 'Error uploading file. Please try again.',
|
||||
}));
|
||||
}, [intl]);
|
||||
|
||||
const onComplete = useCallback((response: ClientResponse) => {
|
||||
cancelUpload.current = undefined;
|
||||
|
|
@ -165,23 +177,12 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
setProgress(1);
|
||||
setFailed(false);
|
||||
setError('');
|
||||
}, []);
|
||||
}, [setBookmark, setUploadError]);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
cancelUpload.current = undefined;
|
||||
setUploadError();
|
||||
}, []);
|
||||
|
||||
const setUploadError = useCallback(() => {
|
||||
setProgress(0);
|
||||
setUploading(false);
|
||||
setFailed(true);
|
||||
|
||||
setError(intl.formatMessage({
|
||||
id: 'channel_bookmark.add.file_upload_error',
|
||||
defaultMessage: 'Error uploading file. Please try again.',
|
||||
}));
|
||||
}, [file, intl]);
|
||||
}, [setUploadError]);
|
||||
|
||||
const startUpload = useCallback((fileInfo: FileInfo | ExtractedFileInfo) => {
|
||||
setUploading(true);
|
||||
|
|
@ -206,7 +207,7 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
setUploadError();
|
||||
cancelUpload.current?.();
|
||||
}
|
||||
}, [channelId, onProgress, onComplete, onError, serverUrl]);
|
||||
}, [serverUrl, channelId, onProgress, onComplete, onError, setUploadError]);
|
||||
|
||||
const browseFile = useCallback(async () => {
|
||||
const picker = new PickerUtil(intl, (files) => {
|
||||
|
|
@ -223,12 +224,12 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
if (res.error) {
|
||||
close();
|
||||
}
|
||||
}, [close, startUpload]);
|
||||
}, [close, intl, startUpload]);
|
||||
|
||||
const removeAndUpload = useCallback(() => {
|
||||
cancelUpload.current?.();
|
||||
browseFile();
|
||||
}, [file, browseFile]);
|
||||
}, [browseFile]);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
cancelUpload.current?.();
|
||||
|
|
@ -237,7 +238,7 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
}
|
||||
}, [file, startUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
if (!initialFile) {
|
||||
browseFile();
|
||||
}
|
||||
|
|
@ -245,7 +246,7 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
return () => {
|
||||
cancelUpload.current?.();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (uploading) {
|
||||
|
|
@ -260,6 +261,10 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
|||
if (!file?.id && file?.name) {
|
||||
setBookmark(file);
|
||||
}
|
||||
|
||||
// We don't care about `setBookmark` changes as long as
|
||||
// it is up to date when the effect runs.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [file, intl, maxFileSize, uploading]);
|
||||
|
||||
let info;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, View, type AlertButton} from 'react-native';
|
||||
import {SafeAreaView, type Edge} from 'react-native-safe-area-context';
|
||||
|
|
@ -12,6 +12,7 @@ import Button from '@components/button';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
|
|
@ -257,9 +258,9 @@ const ChannelBookmarkAddOrEdit = ({
|
|||
}
|
||||
}, [bookmark, formatMessage, handleDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
enableSaveButton(false);
|
||||
}, []);
|
||||
});
|
||||
|
||||
useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSaveBookmark, [bookmark]);
|
||||
useNavButtonPressed(closeButtonId, componentId, close, [close]);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const GroupMessage = ({currentUserId, displayName, members}: Props) => {
|
|||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const userIds = useMemo(() => members.map((cm) => cm.userId).filter((id) => id !== currentUserId),
|
||||
[members.length, currentUserId]);
|
||||
[members, currentUserId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
|||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -91,7 +92,7 @@ const ConvertGMToChannel = ({
|
|||
|
||||
const loadingAnimationTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
loadingAnimationTimeoutRef.current = setTimeout(() => setLoadingAnimationTimeout(true), loadingIndicatorTimeout);
|
||||
async function work() {
|
||||
const {teams} = await fetchGroupMessageMembersCommonTeams(serverUrl, channelId);
|
||||
|
|
@ -107,15 +108,15 @@ const ConvertGMToChannel = ({
|
|||
return () => {
|
||||
clearTimeout(loadingAnimationTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
mounted.current = true;
|
||||
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUserId) {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const CreateOrEditChannel = ({
|
|||
base.showAsAction = 'always';
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [editing, theme.sidebarHeaderTextColor, intl, canSave]);
|
||||
}, [editing, formatMessage, canSave, theme.sidebarHeaderTextColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
|
|
@ -148,7 +148,7 @@ const CreateOrEditChannel = ({
|
|||
leftButtons: [makeCloseButton(icon)],
|
||||
});
|
||||
}
|
||||
}, [theme, isModal]);
|
||||
}, [theme, isModal, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
setCanSave(
|
||||
|
|
@ -159,7 +159,7 @@ const CreateOrEditChannel = ({
|
|||
type !== channel.type
|
||||
),
|
||||
);
|
||||
}, [channel, displayName, purpose, header, type]);
|
||||
}, [channel, displayName, purpose, header, type, channelInfo]);
|
||||
|
||||
const isValidDisplayName = useCallback((): boolean => {
|
||||
if (isDirect(channel)) {
|
||||
|
|
@ -175,7 +175,7 @@ const CreateOrEditChannel = ({
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [channel, displayName]);
|
||||
}, [channel, displayName, intl]);
|
||||
|
||||
const onCreateChannel = useCallback(async () => {
|
||||
dispatch({type: RequestActions.START});
|
||||
|
|
@ -197,7 +197,7 @@ const CreateOrEditChannel = ({
|
|||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId, isModal);
|
||||
switchToChannelById(serverUrl, createdChannel.channel!.id, createdChannel.channel!.team_id);
|
||||
}, [serverUrl, type, displayName, header, isModal, purpose, isValidDisplayName]);
|
||||
}, [isValidDisplayName, serverUrl, displayName, purpose, header, type, componentId, isModal]);
|
||||
|
||||
const onUpdateChannel = useCallback(async () => {
|
||||
if (!channel) {
|
||||
|
|
@ -228,11 +228,11 @@ const CreateOrEditChannel = ({
|
|||
}
|
||||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId, isModal);
|
||||
}, [channel?.id, channel?.type, displayName, header, isModal, purpose, isValidDisplayName]);
|
||||
}, [channel, isValidDisplayName, header, displayName, purpose, serverUrl, componentId, isModal]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close(componentId, isModal);
|
||||
}, [isModal]);
|
||||
}, [componentId, isModal]);
|
||||
|
||||
useNavButtonPressed(CLOSE_BUTTON_ID, componentId, handleClose, [handleClose]);
|
||||
useNavButtonPressed(CREATE_BUTTON_ID, componentId, onCreateChannel, [onCreateChannel]);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {useTheme} from '@context/theme';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useInputPropagation} from '@hooks/input';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
|
|
@ -122,12 +123,9 @@ const EditPost = ({
|
|||
return !hasUploadingFiles && !tooLong && (messageChanged || filesChanged);
|
||||
}, [postMessage, postFiles, editingMessage, maxPostSize, files]);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
toggleSaveButton(false);
|
||||
|
||||
// No dependencies to avoid unnecessary re-renders since this is a one-time effect
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ const EditPostInput = ({
|
|||
const disableCopyAndPaste = managedConfig.copyAndPasteProtection === 'true';
|
||||
const focus = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [inputRef]);
|
||||
|
||||
const updateValue = useCallback((valueOrUpdater: string | ((prevValue: string) => string)) => {
|
||||
if (typeof valueOrUpdater === 'function') {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const EditProfilePicture = ({user, onUpdateProfilePicture}: ChangeProfilePicture
|
|||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [pictureUrl]);
|
||||
}, [pictureUrl, serverUrl]);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const EmojiPickerScreen = ({closeButtonId, componentId, file, imageUrl, onEmojiP
|
|||
const handleEmojiPress = useCallback((emoji: string) => {
|
||||
onEmojiPress(emoji);
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
}, []);
|
||||
}, [onEmojiPress]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
|
|
@ -44,7 +44,7 @@ const EmojiPickerScreen = ({closeButtonId, componentId, file, imageUrl, onEmojiP
|
|||
testID='emoji_picker'
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
}, [file, handleEmojiPress, imageUrl]);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
|
|
|
|||
|
|
@ -35,10 +35,7 @@ const PickerFooter = (props: BottomSheetFooterProps) => {
|
|||
waitForSheetExtended(animatedSheetState, () => {
|
||||
selectEmojiCategoryBarSection(index);
|
||||
});
|
||||
|
||||
// animatedSheetState and expand are stable and don't need to be in dependencies to avoid unnecessary callback recreation
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [animatedSheetState, expand]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const paddingBottom = withTiming(
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ const PickerHeader = ({skinTone, ...props}: Props) => {
|
|||
|
||||
const onBlur = useCallback(() => {
|
||||
isSearching.value = false;
|
||||
}, []);
|
||||
}, [isSearching]);
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
isSearching.value = true;
|
||||
}, []);
|
||||
}, [isSearching]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
containerWidth.value = e.nativeEvent.layout.width;
|
||||
}, []);
|
||||
}, [containerWidth]);
|
||||
|
||||
let search;
|
||||
if (isTablet) {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const SkinSelector = ({onSelectSkin, selected, skins}: Props) => {
|
|||
const code = Object.keys(skinCodes).find((key) => skinCodes[key] === skin) || 'default';
|
||||
await savePreferredSkinTone(serverUrl, code);
|
||||
onSelectSkin();
|
||||
}, [serverUrl]);
|
||||
}, [onSelectSkin, serverUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {InteractionManager, Platform, StyleSheet} from 'react-native';
|
||||
import Animated, {
|
||||
type EntryAnimationsValues, type ExitAnimationsValues, FadeIn, FadeOut,
|
||||
|
|
@ -12,6 +12,7 @@ import Tooltip from 'react-native-walkthrough-tooltip';
|
|||
import {storeSkinEmojiSelectorTutorial} from '@actions/app/global';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
import {skinCodes} from '@utils/emoji';
|
||||
|
||||
import CloseButton from './close_button';
|
||||
|
|
@ -123,16 +124,13 @@ const SkinToneSelector = ({skinTone = 'default', containerWidth, isSearching, tu
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (!tutorialWatched) {
|
||||
setTooltipVisible(true);
|
||||
}
|
||||
});
|
||||
|
||||
// tutorialWatched is not a dependency because it is not used in the effect
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
|||
const close = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
return dismissModal({componentId});
|
||||
}, []);
|
||||
}, [componentId]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dismissModal({componentId});
|
||||
}, []);
|
||||
}, [componentId]);
|
||||
|
||||
const onChangeText = useCallback((text: string) => {
|
||||
setTerm(text);
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ const QuickOptions = ({canCreateChannels, canJoinChannels, close}: Props) => {
|
|||
showModal(Screens.BROWSE_CHANNELS, title, {
|
||||
closeButton,
|
||||
});
|
||||
}, [intl, theme]);
|
||||
}, [close, intl, theme.sidebarHeaderTextColor]);
|
||||
|
||||
const createNewChannel = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'mobile.create_channel.title', defaultMessage: 'New channel'});
|
||||
|
||||
await close();
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title);
|
||||
}, [intl]);
|
||||
}, [close, intl]);
|
||||
|
||||
const openDirectMessage = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'create_direct_message.title', defaultMessage: 'Create Direct Message'});
|
||||
|
|
@ -61,7 +61,7 @@ const QuickOptions = ({canCreateChannels, canJoinChannels, close}: Props) => {
|
|||
showModal(Screens.CREATE_DIRECT_MESSAGE, title, {
|
||||
closeButton,
|
||||
});
|
||||
}, [intl, theme]);
|
||||
}, [close, intl, theme.sidebarHeaderTextColor]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {fetchPublicLink} from '@actions/remote/file';
|
|||
import Toast from '@components/toast';
|
||||
import {GALLERY_FOOTER_HEIGHT} from '@constants/gallery';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useDidMount from '@hooks/did_mount';
|
||||
|
||||
import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery';
|
||||
|
||||
|
|
@ -67,14 +68,14 @@ const CopyPublicLink = ({item, galleryView = true, setAction}: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useDidMount(() => {
|
||||
mounted.current = true;
|
||||
copyLink();
|
||||
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (showToast === false) {
|
||||
|
|
@ -84,6 +85,10 @@ const CopyPublicLink = ({item, galleryView = true, setAction}: Props) => {
|
|||
}
|
||||
}, 350);
|
||||
}
|
||||
|
||||
// This effect controls the timeout of the toast, so
|
||||
// it should only run when `showToast` changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showToast]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue