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