diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index d06e12fe1..ce59bccdc 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,169 +1,187 @@ // @flow import * as React from 'react'; import { FlatList } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useSelector } from 'react-redux'; import { childThreadInfos, communityThreadSelector, } from 'lib/selectors/thread-selectors'; +import { threadIsChannel } from 'lib/shared/thread-utils'; import { type ThreadInfo, communitySubthreads } from 'lib/types/thread-types'; import { useNavigateToThread } from '../chat/message-list-types'; import { useStyles } from '../themes/colors'; import type { TextStyle } from '../types/styles'; import CommunityDrawerItemCommunity from './community-drawer-item-cummunity.react'; const maxDepth = 2; const safeAreaEdges = ['top']; function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const communitiesSuffixed = React.useMemo(() => appendSuffix(communities), [ communities, ]); const styles = useStyles(unboundStyles); const [openCommunity, setOpenCommunity] = React.useState( communitiesSuffixed.length === 1 ? communitiesSuffixed[0].id : null, ); const navigateToThread = useNavigateToThread(); const childThreadInfosMap = useSelector(childThreadInfos); const setOpenCommunnityOrClose = React.useCallback((index: string) => { setOpenCommunity(open => (open === index ? null : index)); }, []); const renderItem = React.useCallback( ({ item }) => { const itemData = { threadInfo: item.threadInfo, itemChildren: item.itemChildren, labelStyle: item.labelStyle, + hasSubchannelsButton: item.subchannelsButton, }; return ( ); }, [navigateToThread, openCommunity, setOpenCommunnityOrClose], ); const labelStylesObj = useStyles(labelUnboundStyles); const labelStyles = React.useMemo( () => [ labelStylesObj.level0Label, labelStylesObj.level1Label, labelStylesObj.level2Label, ], [labelStylesObj], ); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, ), [childThreadInfosMap, communitiesSuffixed, labelStyles], ); return ( ); } function createRecursiveDrawerItemsData( childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, communities: $ReadOnlyArray, labelStyles: $ReadOnlyArray, ) { const result = communities.map(community => ({ key: community.id, threadInfo: community, itemChildren: [], labelStyle: labelStyles[0], + subchannelsButton: 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, + childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, +) { + if (!childThreadInfosMap[threadInfo.id]?.length) { + return false; + } + return childThreadInfosMap[threadInfo.id].some(thread => + threadIsChannel(thread), + ); +} + function appendSuffix(chats: $ReadOnlyArray): ThreadInfo[] { const result = []; const names = new Map(); for (const chat of chats) { 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; } const unboundStyles = { drawerContent: { flex: 1, paddingRight: 8, paddingTop: 8, backgroundColor: 'drawerBackgroud', }, }; 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/native/navigation/community-drawer-item.react.js b/native/navigation/community-drawer-item.react.js index ecc0cd4f1..cb0b78b0e 100644 --- a/native/navigation/community-drawer-item.react.js +++ b/native/navigation/community-drawer-item.react.js @@ -1,133 +1,153 @@ // @flow import * as React from 'react'; import { View, FlatList, TouchableOpacity } from 'react-native'; import type { ThreadInfo } from 'lib/types/thread-types'; import type { MessageListParams } from '../chat/message-list-types'; import { SingleLine } from '../components/single-line.react'; import { useStyles } from '../themes/colors'; import type { TextStyle } from '../types/styles'; import { ExpandButton, ExpandButtonDisabled } from './expand-buttons.react'; +import SubchannelsButton from './subchannels-button.react'; export type CommunityDrawerItemData = { +threadInfo: ThreadInfo, +itemChildren?: $ReadOnlyArray, +labelStyle: TextStyle, + +hasSubchannelsButton: boolean, }; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +toggleExpanded: (threadID: string) => void, +expanded: boolean, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { - itemData: { threadInfo, itemChildren, labelStyle }, + itemData: { threadInfo, itemChildren, labelStyle, hasSubchannelsButton }, navigateToThread, expanded, toggleExpanded, } = props; const styles = useStyles(unboundStyles); const renderItem = React.useCallback( ({ item }) => ( ), [navigateToThread], ); const children = React.useMemo(() => { if (!expanded) { return null; } + if (hasSubchannelsButton) { + return ( + + + + ); + } return ; - }, [expanded, itemChildren, renderItem]); + }, [ + expanded, + itemChildren, + renderItem, + hasSubchannelsButton, + styles.subchannelsButton, + threadInfo, + ]); const onExpandToggled = React.useCallback(() => { toggleExpanded(threadInfo.id); }, [toggleExpanded, threadInfo.id]); const itemExpandButton = React.useMemo(() => { - if (!itemChildren?.length) { + if (!itemChildren?.length && !hasSubchannelsButton) { return ; } return ; - }, [itemChildren?.length, expanded, onExpandToggled]); + }, [itemChildren?.length, hasSubchannelsButton, onExpandToggled, expanded]); const onPress = React.useCallback(() => { navigateToThread({ threadInfo }); }, [navigateToThread, threadInfo]); return ( {itemExpandButton} {threadInfo.uiName} {children} ); } const unboundStyles = { chatView: { marginLeft: 16, }, threadEntry: { flexDirection: 'row', marginVertical: 6, }, textTouchableWrapper: { flex: 1, }, + subchannelsButton: { + marginLeft: 24, + marginBottom: 6, + }, }; export type CommunityDrawerItemChatProps = { +itemData: CommunityDrawerItemData, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItemChat( props: CommunityDrawerItemChatProps, ): React.Node { const [expanded, setExpanded] = React.useState(false); const styles = useStyles(unboundStyles); const toggleExpanded = React.useCallback(() => { setExpanded(isExpanded => !isExpanded); }, []); return ( ); } const MemoizedCommunityDrawerItemChat: React.ComponentType = React.memo( CommunityDrawerItemChat, ); const MemoizedCommunityDrawerItem: React.ComponentType = React.memo( CommunityDrawerItem, ); export default MemoizedCommunityDrawerItem;