diff --git a/lib/utils/drawer-utils.react.js b/lib/utils/drawer-utils.react.js new file mode 100644 index 000000000..32ea81385 --- /dev/null +++ b/lib/utils/drawer-utils.react.js @@ -0,0 +1,84 @@ +// @flow + +import { threadIsChannel } from '../shared/thread-utils.js'; +import { + type ThreadInfo, + type ResolvedThreadInfo, + communitySubthreads, +} from '../types/thread-types.js'; + +export type CommunityDrawerItemData = { + +threadInfo: ThreadInfo, + +itemChildren?: $ReadOnlyArray>, + +hasSubchannelsButton: boolean, + +labelStyle: T, +}; + +function createRecursiveDrawerItemsData( + childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, + communities: $ReadOnlyArray, + labelStyles: $ReadOnlyArray, + maxDepth: number, +): $ReadOnlyArray> { + const result = 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, + childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, +): boolean { + if (!childThreadInfosMap[threadInfo.id]?.length) { + return false; + } + return childThreadInfosMap[threadInfo.id].some(thread => + threadIsChannel(thread), + ); +} + +function appendSuffix( + chats: $ReadOnlyArray, +): ResolvedThreadInfo[] { + 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; +} + +export { createRecursiveDrawerItemsData, appendSuffix }; diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index 60d2d1a1f..4bd8b2662 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,199 +1,122 @@ // @flow import * as React from 'react'; import { FlatList, Platform } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useSelector } from 'react-redux'; import { childThreadInfos, communityThreadSelector, } from 'lib/selectors/thread-selectors.js'; -import { threadIsChannel } from 'lib/shared/thread-utils.js'; import { - type ThreadInfo, - type ResolvedThreadInfo, - communitySubthreads, -} from 'lib/types/thread-types.js'; + createRecursiveDrawerItemsData, + appendSuffix, +} 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 { useNavigateToThread } from '../chat/message-list-types.js'; import { useStyles } from '../themes/colors.js'; -import type { TextStyle } from '../types/styles.js'; const maxDepth = 2; const safeAreaEdges = Platform.select({ ios: ['top'], default: ['top', 'bottom'], }); function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const resolvedCommunities = useResolvedThreadInfos(communities); const communitiesSuffixed = React.useMemo( () => appendSuffix(resolvedCommunities), [resolvedCommunities], ); const styles = useStyles(unboundStyles); const [openCommunity, setOpenCommunity] = React.useState( communitiesSuffixed.length === 1 ? communitiesSuffixed[0].id : null, ); const navigateToThread = useNavigateToThread(); const childThreadInfosMap = useSelector(childThreadInfos); const setOpenCommunityOrClose = 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 ( - - ); - }, + ({ item }) => ( + + ), [navigateToThread, openCommunity, setOpenCommunityOrClose], ); const labelStylesObj = useStyles(labelUnboundStyles); const labelStyles = React.useMemo( () => [ labelStylesObj.level0Label, labelStylesObj.level1Label, labelStylesObj.level2Label, ], [labelStylesObj], ); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, + maxDepth, ), [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, -): ResolvedThreadInfo[] { - 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: 'drawerBackground', }, }; 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 61282b55a..d23c26109 100644 --- a/native/navigation/community-drawer-item.react.js +++ b/native/navigation/community-drawer-item.react.js @@ -1,155 +1,148 @@ // @flow import * as React from 'react'; import { View, FlatList, TouchableOpacity } from 'react-native'; -import type { ThreadInfo } from 'lib/types/thread-types.js'; +import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { ExpandButton, ExpandButtonDisabled } from './expand-buttons.react.js'; import SubchannelsButton from './subchannels-button.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import { SingleLine } from '../components/single-line.react.js'; import { useStyles } from '../themes/colors.js'; import type { TextStyle } from '../types/styles.js'; -export type CommunityDrawerItemData = { - +threadInfo: ThreadInfo, - +itemChildren?: $ReadOnlyArray, - +labelStyle: TextStyle, - +hasSubchannelsButton: boolean, -}; - export type DrawerItemProps = { - +itemData: CommunityDrawerItemData, + +itemData: CommunityDrawerItemData, +toggleExpanded: (threadID: string) => void, +expanded: boolean, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { 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, hasSubchannelsButton, styles.subchannelsButton, threadInfo, ]); const onExpandToggled = React.useCallback(() => { toggleExpanded(threadInfo.id); }, [toggleExpanded, threadInfo.id]); const itemExpandButton = React.useMemo(() => { if (!itemChildren?.length && !hasSubchannelsButton) { return ; } return ; }, [itemChildren?.length, hasSubchannelsButton, onExpandToggled, expanded]); const onPress = React.useCallback(() => { navigateToThread({ threadInfo }); }, [navigateToThread, threadInfo]); const { uiName } = useResolvedThreadInfo(threadInfo); return ( {itemExpandButton} {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, + +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;