diff --git a/app/components/autocomplete/at_mention/at_mention.tsx b/app/components/autocomplete/at_mention/at_mention.tsx index 825b52ca4..148500c06 100644 --- a/app/components/autocomplete/at_mention/at_mention.tsx +++ b/app/components/autocomplete/at_mention/at_mention.tsx @@ -152,7 +152,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { }; }); -const emptyList: UserProfile[] = []; +const emptyProfileList: UserProfile[] = []; +const empytSectionList: UserMentionSections = []; +const emptyGroupList: Group[] = []; const AtMention = ({ channelId, @@ -170,20 +172,20 @@ const AtMention = ({ const theme = useTheme(); const style = getStyleFromTheme(theme); - const [sections, setSections] = useState([]); - const [usersInChannel, setUsersInChannel] = useState([]); - const [usersOutOfChannel, setUsersOutOfChannel] = useState([]); - const [groups, setGroups] = useState([]); + const [sections, setSections] = useState(empytSectionList); + const [usersInChannel, setUsersInChannel] = useState(emptyProfileList); + const [usersOutOfChannel, setUsersOutOfChannel] = useState(emptyProfileList); + const [groups, setGroups] = useState(emptyGroupList); const [loading, setLoading] = useState(false); const [noResultsTerm, setNoResultsTerm] = useState(null); const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes. const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => { setLoading(true); - const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId); - if (!error) { - setUsersInChannel(receivedUsers!.users); - setUsersOutOfChannel(receivedUsers!.out_of_channel || emptyList); + const {users: receivedUsers} = await searchUsers(sUrl, term, cId); + if (receivedUsers) { + setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyProfileList); + setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyProfileList); } setLoading(false); }, 200), []); @@ -195,9 +197,9 @@ const AtMention = ({ const matchTerm = getMatchTermForAtMention(value.substring(0, localCursorPosition), isSearch); const resetState = () => { - setUsersInChannel(emptyList); - setUsersOutOfChannel(emptyList); - setSections([]); + setUsersInChannel(emptyProfileList); + setUsersOutOfChannel(emptyProfileList); + setSections(empytSectionList); runSearch.cancel(); }; @@ -222,7 +224,7 @@ const AtMention = ({ onShowingChange(false); setNoResultsTerm(mention); - setSections([]); + setSections(empytSectionList); }, [value, localCursorPosition, isSearch]); const renderSpecialMentions = useCallback((item: SpecialMention) => { @@ -286,12 +288,12 @@ const AtMention = ({ useEffect(() => { if (useGroupMentions) { getGroupsForAutocomplete(serverUrl, channelId || '').then((res) => { - setGroups(res); + setGroups(res.length ? res : emptyGroupList); }).catch(() => { - setGroups([]); + setGroups(emptyGroupList); }); } else { - setGroups([]); + setGroups(emptyGroupList); } }, [channelId, useGroupMentions]); @@ -318,7 +320,7 @@ const AtMention = ({ if (!loading && !nSections && noResultsTerm == null) { setNoResultsTerm(matchTerm); } - setSections(newSections); + setSections(nSections ? newSections : empytSectionList); onShowingChange(Boolean(nSections)); }, [usersInChannel, usersOutOfChannel, teamMembers, groups, loading]); diff --git a/app/components/autocomplete/channel_mention/channel_mention.tsx b/app/components/autocomplete/channel_mention/channel_mention.tsx index bc6dcaa64..05bf2a8b0 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.tsx +++ b/app/components/autocomplete/channel_mention/channel_mention.tsx @@ -161,6 +161,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { }; }); +const emptySections: Array> = []; +const emptyChannels: Channel[] = []; + const ChannelMention = ({ cursorPosition, isSearch, @@ -175,8 +178,8 @@ const ChannelMention = ({ const theme = useTheme(); const style = getStyleFromTheme(theme); - const [sections, setSections] = useState>>([]); - const [channels, setChannels] = useState([]); + const [sections, setSections] = useState>>(emptySections); + const [channels, setChannels] = useState(emptyChannels); const [loading, setLoading] = useState(false); const [noResultsTerm, setNoResultsTerm] = useState(null); const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes. @@ -187,17 +190,17 @@ const ChannelMention = ({ const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => { setLoading(true); - const {channels: receivedChannels, error} = await searchChannels(sUrl, term); - if (!error) { - setChannels(receivedChannels!); + const {channels: receivedChannels} = await searchChannels(sUrl, term); + if (receivedChannels) { + setChannels(receivedChannels.length ? receivedChannels : emptyChannels); } setLoading(false); }, 200), []); const matchTerm = getMatchTermForChannelMention(value.substring(0, localCursorPosition), isSearch); const resetState = () => { - setChannels([]); - setSections([]); + setChannels(emptyChannels); + setSections(emptySections); runSearch.cancel(); }; @@ -235,7 +238,7 @@ const ChannelMention = ({ onShowingChange(false); setNoResultsTerm(mention); - setSections([]); + setSections(emptySections); }, [value, localCursorPosition, isSearch]); const renderItem = useCallback(({item}) => { @@ -286,7 +289,7 @@ const ChannelMention = ({ if (!loading && !nSections && noResultsTerm == null) { setNoResultsTerm(matchTerm); } - setSections(newSections); + setSections(newSections.length ? newSections : emptySections); onShowingChange(Boolean(nSections)); }, [channels, myMembers, loading]); diff --git a/app/components/navigation_header/header.tsx b/app/components/navigation_header/header.tsx index 819d03ef4..8b18a34d8 100644 --- a/app/components/navigation_header/header.tsx +++ b/app/components/navigation_header/header.tsx @@ -208,5 +208,5 @@ const Header = ({ ); }; -export default Header; +export default React.memo(Header); diff --git a/app/components/post_draft/draft_handler/index.ts b/app/components/post_draft/draft_handler/index.ts index 15293c40a..1dc3bdc8a 100644 --- a/app/components/post_draft/draft_handler/index.ts +++ b/app/components/post_draft/draft_handler/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {combineLatest, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -42,4 +43,4 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { }; }); -export default withDatabase(enhanced(DraftHandler)); +export default React.memo(withDatabase(enhanced(DraftHandler))); diff --git a/app/components/post_draft/index.ts b/app/components/post_draft/index.ts index eeffba879..a89a69e9b 100644 --- a/app/components/post_draft/index.ts +++ b/app/components/post_draft/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {combineLatest, of as of$, from as from$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -85,4 +86,4 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) => }; }); -export default withDatabase(enhanced(PostDraft)); +export default React.memo(withDatabase(enhanced(PostDraft))); diff --git a/app/components/post_draft/post_input/index.ts b/app/components/post_draft/post_input/index.ts index b01d4f695..4255e43fc 100644 --- a/app/components/post_draft/post_input/index.ts +++ b/app/components/post_draft/post_input/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -59,4 +60,4 @@ const enhanced = withObservables([], ({database, channelId, rootId}: WithDatabas }; }); -export default withDatabase(enhanced(PostInput)); +export default React.memo(withDatabase(enhanced(PostInput))); diff --git a/app/components/post_draft/quick_actions/index.ts b/app/components/post_draft/quick_actions/index.ts index f2d159818..e23f63773 100644 --- a/app/components/post_draft/quick_actions/index.ts +++ b/app/components/post_draft/quick_actions/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {combineLatest, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -35,4 +36,4 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { }; }); -export default withDatabase(enhanced(QuickActions)); +export default React.memo(withDatabase(enhanced(QuickActions))); diff --git a/app/components/post_draft/typing/index.tsx b/app/components/post_draft/typing/index.tsx index 5e157e6d3..32118ef7e 100644 --- a/app/components/post_draft/typing/index.tsx +++ b/app/components/post_draft/typing/index.tsx @@ -41,7 +41,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -export default function Typing({ +function Typing({ channelId, rootId, }: Props) { @@ -155,3 +155,5 @@ export default function Typing({ ); } + +export default React.memo(Typing); diff --git a/app/components/post_draft/uploads/index.tsx b/app/components/post_draft/uploads/index.tsx index 1e8f0591e..ca7773dae 100644 --- a/app/components/post_draft/uploads/index.tsx +++ b/app/components/post_draft/uploads/index.tsx @@ -68,7 +68,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -export default function Uploads({ +function Uploads({ currentUserId, files, uploadFileError, @@ -174,3 +174,5 @@ export default function Uploads({ ); } + +export default React.memo(Uploads); diff --git a/app/components/post_list/combined_user_activity/index.ts b/app/components/post_list/combined_user_activity/index.ts index 84fd85ecf..05f8fe232 100644 --- a/app/components/post_list/combined_user_activity/index.ts +++ b/app/components/post_list/combined_user_activity/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {combineLatest, from as from$, of as of$} from 'rxjs'; import {map, switchMap} from 'rxjs/operators'; @@ -54,4 +55,4 @@ const withCombinedPosts = withObservables(['postId'], ({database, postId}: WithD }; }); -export default withDatabase(withCombinedPosts(CombinedUserActivity)); +export default React.memo(withDatabase(withCombinedPosts(CombinedUserActivity))); diff --git a/app/components/post_list/post/body/reactions/reaction.tsx b/app/components/post_list/post/body/reactions/reaction.tsx index 7527ff0e1..f428b5081 100644 --- a/app/components/post_list/post/body/reactions/reaction.tsx +++ b/app/components/post_list/post/body/reactions/reaction.tsx @@ -68,6 +68,8 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re onPress(emojiName, highlight); }, [highlight]); + const fontStyle = useMemo(() => [styles.count, (highlight && styles.countHighlight)], [styles, highlight]); + return ( @@ -95,4 +97,4 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re ); }; -export default Reaction; +export default React.memo(Reaction); diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index 8abeb0cc7..9abaf4220 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; import {Keyboard, TouchableOpacity, View} from 'react-native'; @@ -11,6 +11,7 @@ import {Screens} from '@constants'; import {MAX_ALLOWED_REACTIONS} from '@constants/emoji'; import {useServerUrl} from '@context/server'; import {useIsTablet} from '@hooks/device'; +import useDidUpdate from '@hooks/did_update'; import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation'; import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {preventDoubleTap} from '@utils/tap'; @@ -66,7 +67,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName)))); const styles = getStyleSheet(theme); - useEffect(() => { + useDidUpdate(() => { // This helps keep the reactions in the same position at all times until unmounted const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName)); const sorted = new Set([...sortedReactions]); diff --git a/app/components/post_list/post/index.ts b/app/components/post_list/post/index.ts index d79b8060b..5b46fc1ff 100644 --- a/app/components/post_list/post/index.ts +++ b/app/components/post_list/post/index.ts @@ -3,6 +3,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {from as from$, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -141,4 +142,4 @@ const withPost = withObservables( }; }); -export default withDatabase(withSystem(withPost(Post))); +export default React.memo(withDatabase(withSystem(withPost(Post)))); diff --git a/app/screens/channel/channel_post_list/index.ts b/app/screens/channel/channel_post_list/index.ts index 240eb660a..2e048a339 100644 --- a/app/screens/channel/channel_post_list/index.ts +++ b/app/screens/channel/channel_post_list/index.ts @@ -4,6 +4,7 @@ import {Q} from '@nozbe/watermelondb'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import React from 'react'; import {AppStateStatus} from 'react-native'; import {of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -47,4 +48,4 @@ const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({dat }; }); -export default withDatabase(enhanced(ChannelPostList)); +export default React.memo(withDatabase(enhanced(ChannelPostList))); diff --git a/package-lock.json b/package-lock.json index 5bdcc4a99..b66738dfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "mock-async-storage": "2.2.0", "nock": "13.2.4", "patch-package": "6.4.7", + "react-devtools-core": "4.24.3", "react-native-svg-transformer": "1.0.0", "react-test-renderer": "17.0.2", "tough-cookie": "4.0.0", @@ -19518,9 +19519,10 @@ } }, "node_modules/react-devtools-core": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.19.1.tgz", - "integrity": "sha512-2wJiGffPWK0KggBjVwnTaAk+Z3MSxKInHmdzPTrBh1mAarexsa93Kw+WMX88+XjN+TtYgAiLe9xeTqcO5FfJTw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.3.tgz", + "integrity": "sha512-+htKZxLxDN14jhRG3+IXRiJqNSGHUiPYrMtv9e7qlZxcbKeJjVs+C/hd8kZF5rydp3faBwFN6ZpTaZnLA3/ZGA==", + "dev": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -20224,6 +20226,35 @@ "node": ">= 10" } }, + "node_modules/react-native/node_modules/react-devtools-core": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.19.1.tgz", + "integrity": "sha512-2wJiGffPWK0KggBjVwnTaAk+Z3MSxKInHmdzPTrBh1mAarexsa93Kw+WMX88+XjN+TtYgAiLe9xeTqcO5FfJTw==", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-native/node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/react-native/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -24181,6 +24212,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "dev": true, "engines": { "node": ">=8.3.0" }, @@ -39314,9 +39346,10 @@ } }, "react-devtools-core": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.19.1.tgz", - "integrity": "sha512-2wJiGffPWK0KggBjVwnTaAk+Z3MSxKInHmdzPTrBh1mAarexsa93Kw+WMX88+XjN+TtYgAiLe9xeTqcO5FfJTw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.3.tgz", + "integrity": "sha512-+htKZxLxDN14jhRG3+IXRiJqNSGHUiPYrMtv9e7qlZxcbKeJjVs+C/hd8kZF5rydp3faBwFN6ZpTaZnLA3/ZGA==", + "dev": true, "requires": { "shell-quote": "^1.6.1", "ws": "^7" @@ -39458,6 +39491,23 @@ "react-is": "^17.0.1" } }, + "react-devtools-core": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.19.1.tgz", + "integrity": "sha512-2wJiGffPWK0KggBjVwnTaAk+Z3MSxKInHmdzPTrBh1mAarexsa93Kw+WMX88+XjN+TtYgAiLe9xeTqcO5FfJTw==", + "requires": { + "shell-quote": "^1.6.1", + "ws": "^7" + }, + "dependencies": { + "ws": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "requires": {} + } + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -43031,6 +43081,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "dev": true, "requires": {} }, "xcode": { diff --git a/package.json b/package.json index 9dd006532..9998d029f 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "mock-async-storage": "2.2.0", "nock": "13.2.4", "patch-package": "6.4.7", + "react-devtools-core": "4.24.3", "react-native-svg-transformer": "1.0.0", "react-test-renderer": "17.0.2", "tough-cookie": "4.0.0",