Add sorting of chats (#302)

* add sorting of chats

* simplify sidebar structure

* move categories at the bottom

* fix duplicate fields

* disable context menu for chat group

* rename chat group to chat category

* fix category font color

* show active chat even when category closed
This commit is contained in:
Pavel 2022-08-23 15:48:10 +02:00 committed by GitHub
parent 689a6796e0
commit 5b35c6b73b
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
11 changed files with 209 additions and 156 deletions

View File

@ -1,64 +0,0 @@
import React from 'react'
import * as Collapsible from '@radix-ui/react-collapsible'
import { BellIcon } from '../../../../icons/bell-icon'
import { ChevronDownIcon } from '../../../../icons/chevron-down-icon'
import { styled } from '../../../../styles/config'
import { ContextMenu, ContextMenuTrigger, Text } from '../../../../system'
interface Props {
name: string
children: React.ReactNode
}
export const ChannelGroup = (props: Props) => {
const { name, children } = props
return (
<ContextMenuTrigger>
<Collapsible.Root defaultOpen>
<CollapsibleTrigger>
<Text size="15" weight="500" color="accent">
{name}
</Text>
<ChevronDownIcon />
</CollapsibleTrigger>
<CollapsibleContent>{children}</CollapsibleContent>
</Collapsible.Root>
<ContextMenu>
<ContextMenu.TriggerItem label="Mute Category" icon={<BellIcon />}>
<ContextMenu.Item>For 15 min</ContextMenu.Item>
<ContextMenu.Item>For 1 hour</ContextMenu.Item>
<ContextMenu.Item>For 8 hours</ContextMenu.Item>
<ContextMenu.Item>For 24 hours</ContextMenu.Item>
<ContextMenu.Item>Until I turn it back on</ContextMenu.Item>
</ContextMenu.TriggerItem>
<ContextMenu.Item icon={<BellIcon />}>Mark as Read</ContextMenu.Item>
</ContextMenu>
</ContextMenuTrigger>
)
}
const CollapsibleTrigger = styled(Collapsible.Trigger, {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: 8,
borderRadius: 8,
height: 34,
color: '$accent-1',
'&:hover': {
background: '$gray-3',
},
'&[aria-expanded="true"] svg': {
transform: 'rotate(180deg)',
},
})
const CollapsibleContent = styled(Collapsible.Content, {
overflow: 'hidden',
})

View File

@ -1,22 +0,0 @@
import React from 'react'
// import { ChatMenu } from '../../../../components/chat-menu'
// import { ContextMenuTrigger } from '../../../../system'
import { SidebarItem } from '../sidebar-item'
import type { SidebarItemProps } from '../sidebar-item'
interface Props extends SidebarItemProps {
children: string
}
export const ChannelItem = (props: Props) => {
const { children, ...sidebarItemProps } = props
return (
// <ContextMenuTrigger>
<SidebarItem {...sidebarItemProps}>#{children}</SidebarItem>
// <ChatMenu type="context" />
// </ContextMenuTrigger>
)
}

View File

@ -1,27 +0,0 @@
import React from 'react'
import { useChats } from '../../../../protocol'
import { Box } from '../../../../system'
// import { ChannelGroup } from './channel-group'
import { ChannelItem } from './channel-item'
export const Channels = () => {
const chats = useChats()
return (
<Box css={{ padding: '18px 0', overflow: 'auto' }}>
{chats.map(chat => (
<ChannelItem
key={chat.id}
to={`/${chat.id}`}
unread={false}
muted={false}
name={chat.identity?.displayName}
color={chat.identity?.color}
>
{chat.identity!.displayName}
</ChannelItem>
))}
</Box>
)
}

View File

@ -0,0 +1,78 @@
import React, { useState } from 'react'
import * as Collapsible from '@radix-ui/react-collapsible'
import { ChevronDownIcon } from '../../../../icons/chevron-down-icon'
import { useActiveChat } from '../../../../protocol/use-active-chat'
import { styled } from '../../../../styles/config'
import type { ChatItem } from './chat-item'
interface Props {
name: string
children: React.ReactNode
}
export const ChatCategory = (props: Props) => {
const { name, children } = props
const chat = useActiveChat()
const [open, setOpen] = useState(true)
// show active chat even though the category is closed
const activeChild = React.Children.toArray(children).find(child => {
if (React.isValidElement<React.ComponentProps<typeof ChatItem>>(child)) {
return child.props.chat.id === chat?.uuid
}
})
return (
<>
<Collapsible.Root open={open} onOpenChange={setOpen}>
<CollapsibleTrigger>
{name}
<ChevronDownIcon />
</CollapsibleTrigger>
<CollapsibleContent>{children}</CollapsibleContent>
</Collapsible.Root>
{open === false && activeChild}
</>
)
}
/* <ContextMenuTrigger>
<ContextMenu>
<ContextMenu.TriggerItem label="Mute Category" icon={<BellIcon />}>
<ContextMenu.Item>For 15 min</ContextMenu.Item>
<ContextMenu.Item>For 1 hour</ContextMenu.Item>
<ContextMenu.Item>For 8 hours</ContextMenu.Item>
<ContextMenu.Item>For 24 hours</ContextMenu.Item>
<ContextMenu.Item>Until I turn it back on</ContextMenu.Item>
</ContextMenu.TriggerItem>
<ContextMenu.Item icon={<BellIcon />}>Mark as Read</ContextMenu.Item>
</ContextMenu>
</ContextMenuTrigger> */
const CollapsibleTrigger = styled(Collapsible.Trigger, {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: 8,
borderRadius: 8,
height: 34,
fontWeight: '$500',
color: '$accent-4',
'&:hover': {
background: '$gray-3',
},
'&[aria-expanded="true"] svg': {
transform: 'rotate(180deg)',
},
})
const CollapsibleContent = styled(Collapsible.Content, {
overflow: 'hidden',
})

View File

@ -5,35 +5,35 @@ import { NavLink } from 'react-router-dom'
import { styled } from '../../../../styles/config' import { styled } from '../../../../styles/config'
import { Avatar } from '../../../../system' import { Avatar } from '../../../../system'
import type { Chat } from '../../../../protocol/use-sorted-chats'
import type { Ref } from 'react' import type { Ref } from 'react'
interface Props { interface Props {
to: string chat: Chat
muted: boolean
unread: boolean
children: React.ReactNode
name?: string
color?: string
} }
const SidebarItem = (props: Props, ref: Ref<HTMLAnchorElement>) => { const ChatItem = (props: Props, ref: Ref<HTMLAnchorElement>) => {
const { muted, unread, children, name, color, ...buttonProps } = props const { chat } = props
const muted = false
const unread = false
const { color, displayName } = chat.identity!
return ( return (
<Link <Link
ref={ref} ref={ref}
to={`/${chat.id}`}
state={muted ? 'muted' : unread ? 'unread' : undefined} state={muted ? 'muted' : unread ? 'unread' : undefined}
{...buttonProps}
> >
<Avatar size={24} name={name} color={color} /> <Avatar size={24} name={displayName} color={color} />#{displayName}
{children}
</Link> </Link>
) )
} }
const _SidebarItem = forwardRef(SidebarItem) const _ChatItem = forwardRef(ChatItem)
export { _SidebarItem as SidebarItem } export { _ChatItem as ChatItem }
export type SidebarItemProps = Omit<Props, 'children'> export type SidebarItemProps = Omit<Props, 'children'>
const Link = styled(NavLink, { const Link = styled(NavLink, {

View File

@ -0,0 +1,26 @@
import React from 'react'
import { useSortedChats } from '../../../../protocol'
import { Box } from '../../../../system'
import { ChatCategory } from './chat-category'
import { ChatItem } from './chat-item'
export const Chats = () => {
const { categories, chats } = useSortedChats()
return (
<Box css={{ padding: '18px 0', overflow: 'auto' }}>
{chats.map(chat => (
<ChatItem key={chat.id} chat={chat} />
))}
{categories.map(category => (
<ChatCategory key={category.id} name={category.name}>
{category.chats.map(chat => (
<ChatItem key={chat.id} chat={chat} />
))}
</ChatCategory>
))}
</Box>
)
}

View File

@ -4,7 +4,7 @@ import { useAppState } from '../../contexts/app-context'
import { useAccount } from '../../protocol' import { useAccount } from '../../protocol'
import { styled } from '../../styles/config' import { styled } from '../../styles/config'
import { Separator } from '../../system' import { Separator } from '../../system'
import { Channels } from './components/channels' import { Chats } from './components/chats'
import { CommunityInfo } from './components/community-info' import { CommunityInfo } from './components/community-info'
import { GetStarted } from './components/get-started' import { GetStarted } from './components/get-started'
@ -19,7 +19,7 @@ export const MainSidebar = () => {
return ( return (
<Wrapper> <Wrapper>
<CommunityInfo /> <CommunityInfo />
<Channels /> <Chats />
{!account && ( {!account && (
<> <>

View File

@ -3,8 +3,8 @@ export type { Account } from './use-account'
export { useAccount } from './use-account' export { useAccount } from './use-account'
export type { Chat } from './use-chat' export type { Chat } from './use-chat'
export { useChat } from './use-chat' export { useChat } from './use-chat'
export { useChats } from './use-chats'
export type { Member } from './use-members' export type { Member } from './use-members'
export { useMembers } from './use-members' export { useMembers } from './use-members'
export type { Message, Reaction, Reactions } from './use-messages' export type { Message, Reaction, Reactions } from './use-messages'
export { useMessages } from './use-messages' export { useMessages } from './use-messages'
export { useSortedChats } from './use-sorted-chats'

View File

@ -0,0 +1,12 @@
import { useMatch } from 'react-router-dom'
import { useProtocol } from './provider'
export const useActiveChat = () => {
const { client } = useProtocol()
const { params } = useMatch(':id')!
const chatId = params.id!
return client.community.getChat(chatId)
}

View File

@ -1,27 +0,0 @@
import { useMemo } from 'react'
import { useProtocol } from './provider'
import type { Community } from '@status-im/js'
export type Chat = Community['description']['chats'][0] & {
id: string
}
export const useChats = (): Chat[] => {
const { community } = useProtocol()
return useMemo(() => {
return Object.entries(community.chats)
.map(([chatId, chat]) => ({ id: chatId, ...chat }))
.sort((a, b) => {
if (a.position < b.position) {
return -1
}
if (a.position > b.position) {
return 1
}
return 0
})
}, [community])
}

View File

@ -0,0 +1,77 @@
import { useMemo } from 'react'
import { useProtocol } from './provider'
import type { Community } from '@status-im/js'
export type Chat = Community['description']['chats'][0] & {
id: string
}
export type Category = {
id: string
name: string
position: number
chats: Chat[]
}
type Result = {
categories: Category[]
chats: Chat[]
}
function sortByPosition<T extends { position: number }>(items: T[]): T[] {
items.sort((a, b) => {
if (a.position < b.position) {
return -1
}
if (a.position > b.position) {
return 1
}
return 0
})
return items
}
export const useSortedChats = (): Result => {
const { community } = useProtocol()
return useMemo<Result>(() => {
const categoryChats: Record<string, Chat[]> = {}
const chats = Object.entries(community.chats).reduce<Chat[]>(
(acc, [chatId, chat]) => {
const parsedChat: Chat = {
id: chatId,
...chat,
}
if (chat.categoryId && community.categories[chat.categoryId]) {
categoryChats[chat.categoryId] ??= []
categoryChats[chat.categoryId].push(parsedChat)
} else {
acc.push(parsedChat)
}
return acc
},
[]
)
const categories = Object.entries(categoryChats).map(([id, chats]) => {
const { name, position } = community.categories[id]
return {
id,
name,
position,
chats: sortByPosition(chats),
}
})
return {
categories: sortByPosition(categories),
chats: sortByPosition(chats),
}
}, [community])
}