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
689a6796e0
commit
5b35c6b73b
|
@ -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 { 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, {
|
|
@ -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 { 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 && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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