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:
parent
1b50001204
commit
5a081466fe
|
@ -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',
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
})
|
|
@ -5,35 +5,35 @@ import { NavLink } from 'react-router-dom'
|
|||
import { styled } from '../../../../styles/config'
|
||||
import { Avatar } from '../../../../system'
|
||||
|
||||
import type { Chat } from '../../../../protocol/use-sorted-chats'
|
||||
import type { Ref } from 'react'
|
||||
|
||||
interface Props {
|
||||
to: string
|
||||
muted: boolean
|
||||
unread: boolean
|
||||
children: React.ReactNode
|
||||
name?: string
|
||||
color?: string
|
||||
chat: Chat
|
||||
}
|
||||
|
||||
const SidebarItem = (props: Props, ref: Ref<HTMLAnchorElement>) => {
|
||||
const { muted, unread, children, name, color, ...buttonProps } = props
|
||||
const ChatItem = (props: Props, ref: Ref<HTMLAnchorElement>) => {
|
||||
const { chat } = props
|
||||
|
||||
const muted = false
|
||||
const unread = false
|
||||
|
||||
const { color, displayName } = chat.identity!
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
to={`/${chat.id}`}
|
||||
state={muted ? 'muted' : unread ? 'unread' : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
<Avatar size={24} name={name} color={color} />
|
||||
{children}
|
||||
<Avatar size={24} name={displayName} color={color} />#{displayName}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const _SidebarItem = forwardRef(SidebarItem)
|
||||
const _ChatItem = forwardRef(ChatItem)
|
||||
|
||||
export { _SidebarItem as SidebarItem }
|
||||
export { _ChatItem as ChatItem }
|
||||
export type SidebarItemProps = Omit<Props, 'children'>
|
||||
|
||||
const Link = styled(NavLink, {
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -4,7 +4,7 @@ import { useAppState } from '../../contexts/app-context'
|
|||
import { useAccount } from '../../protocol'
|
||||
import { styled } from '../../styles/config'
|
||||
import { Separator } from '../../system'
|
||||
import { Channels } from './components/channels'
|
||||
import { Chats } from './components/chats'
|
||||
import { CommunityInfo } from './components/community-info'
|
||||
import { GetStarted } from './components/get-started'
|
||||
|
||||
|
@ -19,7 +19,7 @@ export const MainSidebar = () => {
|
|||
return (
|
||||
<Wrapper>
|
||||
<CommunityInfo />
|
||||
<Channels />
|
||||
<Chats />
|
||||
|
||||
{!account && (
|
||||
<>
|
||||
|
|
|
@ -3,8 +3,8 @@ export type { Account } from './use-account'
|
|||
export { useAccount } from './use-account'
|
||||
export type { Chat } from './use-chat'
|
||||
export { useChat } from './use-chat'
|
||||
export { useChats } from './use-chats'
|
||||
export type { Member } from './use-members'
|
||||
export { useMembers } from './use-members'
|
||||
export type { Message, Reaction, Reactions } from './use-messages'
|
||||
export { useMessages } from './use-messages'
|
||||
export { useSortedChats } from './use-sorted-chats'
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
Loading…
Reference in New Issue