diff --git a/lib/utils/drawer-utils.react.js b/lib/utils/drawer-utils.react.js index bd854bee4..0801d0043 100644 --- a/lib/utils/drawer-utils.react.js +++ b/lib/utils/drawer-utils.react.js @@ -1,105 +1,105 @@ // @flow import { values } from './objects.js'; import { threadInFilterList, threadIsChannel } from '../shared/thread-utils.js'; import { communitySubthreads } from '../types/thread-types-enum.js'; import type { RawThreadInfo, ThreadInfo, ResolvedThreadInfo, } from '../types/thread-types.js'; export type CommunityDrawerItemData = { +threadInfo: ThreadInfo, - +itemChildren?: $ReadOnlyArray>, + +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; } function filterThreadIDsBelongingToCommunity( communityID: string, threadInfosObj: { +[id: string]: ThreadInfo | RawThreadInfo }, ): $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, appendSuffix, filterThreadIDsBelongingToCommunity, }; diff --git a/web/sidebar/community-drawer-item-community.react.js b/web/sidebar/community-drawer-item-community.react.js index 0f2c3ab77..d995e1bfb 100644 --- a/web/sidebar/community-drawer-item-community.react.js +++ b/web/sidebar/community-drawer-item-community.react.js @@ -1,24 +1,103 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; +import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; + import css from './community-drawer-item.css'; import type { DrawerItemProps } from './community-drawer-item.react.js'; -import CommunityDrawerItem from './community-drawer-item.react.js'; +import { + getChildren, + getExpandButton, +} from './community-drawer-utils.react.js'; +import ThreadAvatar from '../components/thread-avatar.react.js'; function CommunityDrawerItemCommunity(props: DrawerItemProps): React.Node { + const { + itemData: { threadInfo, itemChildren, hasSubchannelsButton, labelStyle }, + expanded, + toggleExpanded, + paddingLeft, + expandable = true, + handler: Handler, + } = props; + + const children = React.useMemo( + () => + getChildren({ + expanded, + hasSubchannelsButton, + itemChildren, + paddingLeft, + threadInfo, + expandable, + handler: Handler, + }), + [ + expanded, + hasSubchannelsButton, + itemChildren, + paddingLeft, + threadInfo, + expandable, + Handler, + ], + ); + + const itemExpandButton = React.useMemo( + () => + getExpandButton({ + expandable, + childrenLength: itemChildren?.length, + hasSubchannelsButton, + expanded, + }), + [expandable, itemChildren?.length, hasSubchannelsButton, expanded], + ); + + const [handler, setHandler] = React.useState({ + // eslint-disable-next-line no-unused-vars + onClick: event => {}, + isActive: false, + }); + + const onClick = React.useCallback( + (event: SyntheticEvent) => { + toggleExpanded?.(threadInfo.id); + handler.onClick(event); + }, + [threadInfo.id, toggleExpanded, handler], + ); + const classes = classnames({ [css.communityBase]: true, [css.communityExpanded]: props.expanded, }); + + const { uiName } = useResolvedThreadInfo(threadInfo); + const titleLabel = classnames({ + [css[labelStyle]]: true, + [css.activeTitle]: handler.isActive, + }); + + const style = React.useMemo(() => ({ paddingLeft }), [paddingLeft]); + return ( ); } const MemoizedCommunityDrawerItemCommunity: React.ComponentType = React.memo(CommunityDrawerItemCommunity); export default MemoizedCommunityDrawerItemCommunity; diff --git a/web/sidebar/community-drawer-item.css b/web/sidebar/community-drawer-item.css index 8e0bd85c3..bcc7a37a3 100644 --- a/web/sidebar/community-drawer-item.css +++ b/web/sidebar/community-drawer-item.css @@ -1,68 +1,67 @@ .threadEntry { display: flex; flex-direction: row; padding-top: 8px; padding-bottom: 8px; } .active { background-color: var(--active-drawer-item-bg); border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .threadListContainer { display: flex; flex-direction: column; } .titleWrapper { overflow: hidden; width: 100%; display: flex; align-items: center; } .title { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; color: var(--drawer-item-color); font-size: var(--s-font-14); font-weight: var(--semi-bold); line-height: 22px; margin-left: 8px; } .activeTitle { color: var(--drawer-active-item-color); } .buttonContainer { width: 24px; align-items: center; justify-content: center; display: flex; } .communityBase { - padding-top: 2px; + padding-top: 4px; padding-bottom: 2px; padding-right: 4px; overflow: hidden; } .communityExpanded { background-color: var(--drawer-open-community-bg); border-top-right-radius: 8px; border-bottom-right-radius: 8px; - padding-top: 4px; padding-bottom: 4px; } .subchannelsButton { margin-bottom: 6px; margin-top: 4px; display: flex; align-items: center; } diff --git a/web/sidebar/community-drawer-item.react.js b/web/sidebar/community-drawer-item.react.js index 0c0b01b71..4e3b7f5f9 100644 --- a/web/sidebar/community-drawer-item.react.js +++ b/web/sidebar/community-drawer-item.react.js @@ -1,177 +1,155 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import type { HandlerProps } from './community-drawer-item-handlers.react.js'; import css from './community-drawer-item.css'; -import { ExpandButton } from './expand-buttons.react.js'; -import SubchannelsButton from './subchannels-button.react.js'; +import { + getChildren, + getExpandButton, +} from './community-drawer-utils.react.js'; import ThreadAvatar from '../components/thread-avatar.react.js'; import { shouldRenderAvatars } from '../utils/avatar-utils.js'; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +toggleExpanded?: (threadID: string) => void, +expanded: boolean, +paddingLeft: number, +expandable?: boolean, +handler: React.ComponentType, }; -const indentation = 14; -const subchannelsButtonIndentation = 24; - function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { itemData: { threadInfo, itemChildren, hasSubchannelsButton, labelStyle }, expanded, toggleExpanded, paddingLeft, expandable = true, handler: Handler, } = props; - const children = React.useMemo(() => { - if (!expanded) { - return null; - } - if (hasSubchannelsButton) { - const buttonPaddingLeft = paddingLeft + subchannelsButtonIndentation; - return ( -
- -
- ); - } - if (!itemChildren) { - return null; - } - return itemChildren.map(item => ( - - )); - }, [ - expanded, - hasSubchannelsButton, - itemChildren, - paddingLeft, - threadInfo, - expandable, - Handler, - ]); + const children = React.useMemo( + () => + getChildren({ + expanded, + hasSubchannelsButton, + itemChildren, + paddingLeft, + threadInfo, + expandable, + handler: Handler, + }), + [ + expanded, + hasSubchannelsButton, + itemChildren, + paddingLeft, + threadInfo, + expandable, + Handler, + ], + ); const onExpandToggled = React.useCallback( () => (toggleExpanded ? toggleExpanded(threadInfo.id) : null), [toggleExpanded, threadInfo.id], ); - const itemExpandButton = React.useMemo(() => { - if (!expandable) { - return null; - } - if (itemChildren?.length === 0 && !hasSubchannelsButton) { - return ( -
- -
- ); - } - return ( -
- -
- ); - }, [ - expandable, - itemChildren?.length, - hasSubchannelsButton, - onExpandToggled, - expanded, - ]); + const itemExpandButton = React.useMemo( + () => + getExpandButton({ + expandable, + childrenLength: itemChildren.length, + hasSubchannelsButton, + onExpandToggled, + expanded, + }), + [ + expandable, + itemChildren.length, + hasSubchannelsButton, + onExpandToggled, + expanded, + ], + ); const [handler, setHandler] = React.useState({ // eslint-disable-next-line no-unused-vars onClick: event => {}, }); const { uiName } = useResolvedThreadInfo(threadInfo); const titleLabel = classnames({ [css[labelStyle]]: true, [css.activeTitle]: handler.isActive, }); const style = React.useMemo(() => ({ paddingLeft }), [paddingLeft]); const threadEntry = classnames({ [css.threadEntry]: true, [css.active]: handler.isActive, }); const titleStyle = React.useMemo( () => ({ marginLeft: shouldRenderAvatars ? 8 : 0, }), [], ); return ( <>
{itemExpandButton}
{uiName}
{children}
); } export type CommunityDrawerItemChatProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, +handler: React.ComponentType, }; function CommunityDrawerItemChat( props: CommunityDrawerItemChatProps, ): React.Node { const [expanded, setExpanded] = React.useState(false); 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; +export default MemoizedCommunityDrawerItemChat; diff --git a/web/sidebar/community-drawer-utils.react.js b/web/sidebar/community-drawer-utils.react.js new file mode 100644 index 000000000..6efcb0a6e --- /dev/null +++ b/web/sidebar/community-drawer-utils.react.js @@ -0,0 +1,89 @@ +// @flow + +import * as React from 'react'; + +import type { ThreadInfo } from 'lib/types/thread-types'; +import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react'; + +import type { HandlerProps } from './community-drawer-item-handlers.react'; +import css from './community-drawer-item.css'; +import CommunityDrawerItemChat from './community-drawer-item.react.js'; +import { ExpandButton } from './expand-buttons.react.js'; +import SubchannelsButton from './subchannels-button.react.js'; + +const indentation = 14; +const subchannelsButtonIndentation = 24; + +function getChildren({ + expanded, + hasSubchannelsButton, + itemChildren, + paddingLeft, + threadInfo, + expandable, + handler, +}: { + expanded: boolean, + hasSubchannelsButton: boolean, + itemChildren: $ReadOnlyArray>, + paddingLeft: number, + threadInfo: ThreadInfo, + expandable: boolean, + handler: React.ComponentType, +}): React.Node { + if (!expanded) { + return null; + } + if (hasSubchannelsButton) { + const buttonPaddingLeft = paddingLeft + subchannelsButtonIndentation; + return ( +
+ +
+ ); + } + return itemChildren.map(item => ( + + )); +} + +function getExpandButton({ + expandable, + childrenLength, + hasSubchannelsButton, + onExpandToggled, + expanded, +}: { + +expandable: boolean, + +childrenLength: ?number, + +hasSubchannelsButton: boolean, + +onExpandToggled?: ?() => ?void, + +expanded: boolean, +}): React.Node { + if (!expandable) { + return null; + } + if (childrenLength === 0 && !hasSubchannelsButton) { + return ( +
+ +
+ ); + } + return ( +
+ +
+ ); +} + +export { getChildren, getExpandButton }; diff --git a/web/sidebar/expand-buttons.react.js b/web/sidebar/expand-buttons.react.js index 8fd70a903..ae5a27658 100644 --- a/web/sidebar/expand-buttons.react.js +++ b/web/sidebar/expand-buttons.react.js @@ -1,33 +1,33 @@ // @flow import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import css from './expand-buttons.css'; import Button from '../components/button.react.js'; type Props = { - +onClick?: () => mixed, + +onClick?: ?() => mixed, +expanded?: boolean, +disabled?: boolean, }; function ExpandButton(props: Props): React.Node { const { onClick, expanded = false, disabled } = props; const icon = expanded ? faCaretDown : faCaretRight; const iconClass = classNames({ [css.disabledButtonIcon]: disabled, [css.buttonIcon]: !disabled, }); return ( ); } export { ExpandButton };