diff --git a/lib/hooks/thread-hooks.js b/lib/hooks/thread-hooks.js new file mode 100644 index 000000000..52246c9b1 --- /dev/null +++ b/lib/hooks/thread-hooks.js @@ -0,0 +1,39 @@ +// @flow + +import * as React from 'react'; + +import { useChatMentionContext } from './chat-mention-hooks.js'; +import { childThreadInfos } from '../selectors/thread-selectors.js'; +import { getCommunity } from '../shared/thread-utils.js'; +import type { ResolvedThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { useSelector } from '../utils/redux-utils.js'; + +function useChildThreadInfosMap(): { + +[id: string]: $ReadOnlyArray, +} { + const childThreadInfosMap = useSelector(childThreadInfos); + + const { chatMentionCandidatesObj } = useChatMentionContext(); + + return React.useMemo(() => { + const result: { [id: string]: $ReadOnlyArray } = {}; + for (const parentThreadID in childThreadInfosMap) { + result[parentThreadID] = childThreadInfosMap[parentThreadID] + .map(rawThreadInfo => { + const community = getCommunity(rawThreadInfo); + if (!community) { + return undefined; + } + const communityThreads = chatMentionCandidatesObj[community]; + if (!communityThreads) { + return undefined; + } + return communityThreads[rawThreadInfo.id]?.threadInfo; + }) + .filter(Boolean); + } + return result; + }, [childThreadInfosMap, chatMentionCandidatesObj]); +} + +export { useChildThreadInfosMap }; diff --git a/lib/utils/drawer-utils.react.js b/lib/utils/drawer-utils.react.js index 17b0372a2..e332fd1e4 100644 --- a/lib/utils/drawer-utils.react.js +++ b/lib/utils/drawer-utils.react.js @@ -1,120 +1,120 @@ // @flow import * as React from 'react'; import { values } from './objects.js'; import { threadInFilterList, threadIsChannel } from '../shared/thread-utils.js'; import type { ResolvedThreadInfo, ThreadInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { communitySubthreads } from '../types/thread-types-enum.js'; type WritableCommunityDrawerItemData = { - threadInfo: ThreadInfo, + threadInfo: ResolvedThreadInfo, itemChildren: $ReadOnlyArray>, hasSubchannelsButton: boolean, labelStyle: T, }; export type CommunityDrawerItemData = $ReadOnly< WritableCommunityDrawerItemData, >; function createRecursiveDrawerItemsData( childThreadInfosMap: { - +[id: string]: $ReadOnlyArray, + +[id: string]: $ReadOnlyArray, }, communities: $ReadOnlyArray, labelStyles: $ReadOnlyArray, maxDepth: number, ): $ReadOnlyArray> { const result: $ReadOnlyArray< WritableCommunityDrawerItemData, > = communities.map(community => ({ threadInfo: community, itemChildren: [], labelStyle: labelStyles[0], hasSubchannelsButton: false, })); let queue = result.map(item => [item, 0]); for (let i = 0; i < queue.length; i++) { const [item, lvl] = queue[i]; const itemChildThreadInfos = childThreadInfosMap[item.threadInfo.id] ?? []; if (lvl < maxDepth) { item.itemChildren = itemChildThreadInfos .filter(childItem => communitySubthreads.includes(childItem.type)) .map(childItem => ({ threadInfo: childItem, itemChildren: [], labelStyle: labelStyles[Math.min(lvl + 1, labelStyles.length - 1)], hasSubchannelsButton: lvl + 1 === maxDepth && threadHasSubchannels(childItem, childThreadInfosMap), })); queue = queue.concat( item.itemChildren.map(childItem => [childItem, lvl + 1]), ); } } return result; } function threadHasSubchannels( - threadInfo: ThreadInfo, + threadInfo: ResolvedThreadInfo, childThreadInfosMap: { - +[id: string]: $ReadOnlyArray, + +[id: string]: $ReadOnlyArray, }, ): boolean { if (!childThreadInfosMap[threadInfo.id]?.length) { return false; } return childThreadInfosMap[threadInfo.id].some(thread => threadIsChannel(thread), ); } function useAppendCommunitySuffix( communities: $ReadOnlyArray, ): $ReadOnlyArray { return React.useMemo(() => { const result: ResolvedThreadInfo[] = []; const names = new Map(); for (const chat of communities) { let name = chat.uiName; const numberOfOccurrences = names.get(name); names.set(name, (numberOfOccurrences ?? 0) + 1); if (numberOfOccurrences) { name = `${name} (${numberOfOccurrences.toString()})`; } result.push({ ...chat, uiName: name }); } return result; }, [communities]); } function filterThreadIDsBelongingToCommunity( communityID: string, threadInfosObj: { +[id: string]: RawThreadInfo | ThreadInfo, }, ): $ReadOnlySet { const threadInfos = values(threadInfosObj); const threadIDs = threadInfos .filter( thread => (thread.community === communityID || thread.id === communityID) && threadInFilterList(thread), ) .map(item => item.id); return new Set(threadIDs); } export { createRecursiveDrawerItemsData, useAppendCommunitySuffix, filterThreadIDsBelongingToCommunity, }; diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index 360b5ef2e..2a0008daa 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,249 +1,247 @@ // @flow import { useDrawerStatus } from '@react-navigation/drawer'; import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { FlatList, Platform, View, Text, TouchableOpacity } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useSelector } from 'react-redux'; import { fetchCommunityInfosActionTypes, useFetchCommunityInfos, } from 'lib/actions/community-actions.js'; import { fetchPrimaryInviteLinkActionTypes, useFetchPrimaryInviteLinks, } from 'lib/actions/link-actions.js'; -import { - childThreadInfos, - communityThreadSelector, -} from 'lib/selectors/thread-selectors.js'; +import { useChildThreadInfosMap } from 'lib/hooks/thread-hooks.js'; +import { communityThreadSelector } from 'lib/selectors/thread-selectors.js'; import { threadTypeIsCommunityRoot } from 'lib/types/thread-types-enum.js'; import { createRecursiveDrawerItemsData, useAppendCommunitySuffix, } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfos } from 'lib/utils/entity-helpers.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import CommunityDrawerItem from './community-drawer-item.react.js'; import { CommunityCreationRouteName } from './route-names.js'; import { useNavigateToThread } from '../chat/message-list-types.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; +import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import { flattenDrawerItemsData, filterOutThreadAndDescendantIDs, type CommunityDrawerItemDataFlattened, } from '../utils/drawer-utils.react.js'; const maxDepth = 2; const safeAreaEdges: $ReadOnlyArray = Platform.select({ ios: ['top'], default: ['top', 'bottom'], }); function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const resolvedCommunities = useResolvedThreadInfos(communities); const communitiesSuffixed = useAppendCommunitySuffix(resolvedCommunities); const styles = useStyles(unboundStyles); const callFetchPrimaryLinks = useFetchPrimaryInviteLinks(); const fetchCommunityInfos = useFetchCommunityInfos(); const dispatchActionPromise = useDispatchActionPromise(); const drawerStatus = useDrawerStatus(); React.useEffect(() => { if (drawerStatus !== 'open') { return; } void dispatchActionPromise( fetchPrimaryInviteLinkActionTypes, callFetchPrimaryLinks(), ); void dispatchActionPromise( fetchCommunityInfosActionTypes, fetchCommunityInfos(), ); }, [ callFetchPrimaryLinks, dispatchActionPromise, drawerStatus, fetchCommunityInfos, ]); const [expanded, setExpanded] = React.useState>(() => { if (communitiesSuffixed.length === 1) { return new Set([communitiesSuffixed[0].id]); } return new Set(); }); const setOpenCommunityOrClose = React.useCallback( (id: string) => expanded.has(id) ? setExpanded(new Set()) : setExpanded(new Set([id])), [expanded], ); const labelStylesObj = useStyles(labelUnboundStyles); const labelStyles = React.useMemo( () => [ labelStylesObj.level0Label, labelStylesObj.level1Label, labelStylesObj.level2Label, ], [labelStylesObj], ); - const childThreadInfosMap = useSelector(childThreadInfos); + const childThreadInfosMap = useChildThreadInfosMap(); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, maxDepth, ), [childThreadInfosMap, communitiesSuffixed, labelStyles], ); const toggleExpanded = React.useCallback( (id: string) => setExpanded(expandedState => { if (expanded.has(id)) { return new Set( filterOutThreadAndDescendantIDs( [...expandedState.values()], drawerItemsData, id, ), ); } return new Set([...expanded.values(), id]); }), [drawerItemsData, expanded], ); const navigateToThread = useNavigateToThread(); const renderItem = React.useCallback( ({ item, }: { +item: CommunityDrawerItemDataFlattened, ... }): React.Node => { const isCommunity = threadTypeIsCommunityRoot(item.threadInfo.type); return ( ); }, [expanded, navigateToThread, setOpenCommunityOrClose, toggleExpanded], ); const { navigate } = useNavigation(); const onPressCommunityCreation = React.useCallback(() => { navigate(CommunityCreationRouteName); }, [navigate]); const communityCreationButton = ( Create community ); const flattenedDrawerItemsData = React.useMemo( () => flattenDrawerItemsData(drawerItemsData, [...expanded.values()]), [drawerItemsData, expanded], ); return ( {communityCreationButton} ); } const unboundStyles = { drawerContent: { flex: 1, paddingRight: 8, paddingTop: 8, paddingBottom: 8, }, communityCreationContainer: { flexDirection: 'row', padding: 24, alignItems: 'center', borderTopWidth: 1, borderColor: 'panelSeparator', }, communityCreationText: { color: 'panelForegroundLabel', fontSize: 16, lineHeight: 24, fontWeight: '500', }, communityCreationIconContainer: { height: 28, width: 28, justifyContent: 'center', alignItems: 'center', borderRadius: 16, marginRight: 12, backgroundColor: 'panelSecondaryForeground', }, communityCreationIcon: { color: 'panelForegroundLabel', }, }; const labelUnboundStyles = { level0Label: { color: 'drawerItemLabelLevel0', fontSize: 16, lineHeight: 24, fontWeight: '500', }, level1Label: { color: 'drawerItemLabelLevel1', fontSize: 14, lineHeight: 22, fontWeight: '500', }, level2Label: { color: 'drawerItemLabelLevel2', fontSize: 14, lineHeight: 22, fontWeight: '400', }, }; const MemoizedCommunityDrawerContent: React.ComponentType<{}> = React.memo( CommunityDrawerContent, ); export default MemoizedCommunityDrawerContent; diff --git a/web/sidebar/community-drawer.react.js b/web/sidebar/community-drawer.react.js index 2a1475b60..a3d085aa7 100644 --- a/web/sidebar/community-drawer.react.js +++ b/web/sidebar/community-drawer.react.js @@ -1,78 +1,76 @@ // @flow import * as React from 'react'; -import { - childThreadInfos, - communityThreadSelector, -} from 'lib/selectors/thread-selectors.js'; +import { useChildThreadInfosMap } from 'lib/hooks/thread-hooks.js'; +import { communityThreadSelector } from 'lib/selectors/thread-selectors.js'; import { createRecursiveDrawerItemsData, useAppendCommunitySuffix, } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfos } from 'lib/utils/entity-helpers.js'; import CommunityDrawerItemCommunity from './community-drawer-item-community.react.js'; import css from './community-drawer.css'; import { ThreadListProvider } from '../chat/thread-list-provider.js'; import { useSelector } from '../redux/redux-utils.js'; const maxDepth = 2; const labelStyles = ['title']; function CommunityDrawer(): React.Node { const tab = useSelector(state => state.navInfo.tab); - const childThreadInfosMap = useSelector(childThreadInfos); + const childThreadInfosMap = useChildThreadInfosMap(); const communities = useSelector(communityThreadSelector); const resolvedCommunities = useResolvedThreadInfos(communities); const communitiesSuffixed = useAppendCommunitySuffix(resolvedCommunities); const drawerItemsData = createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, maxDepth, ); const communitiesComponentsDefault = React.useMemo( () => drawerItemsData.map(item => ( )), [drawerItemsData], ); const communitiesComponentsCal = React.useMemo( () => drawerItemsData.map(item => ( )), [drawerItemsData], ); const defaultStyle = tab === 'calendar' ? css.hidden : null; const calStyle = tab !== 'calendar' ? css.hidden : null; return (
{communitiesComponentsDefault}
{communitiesComponentsCal}
); } export default CommunityDrawer;