Skip to content

Commit ef847c0

Browse files
author
einan
committed
Added sub chat item feature.
1 parent 840cc85 commit ef847c0

5 files changed

Lines changed: 235 additions & 110 deletions

File tree

example/components/ChatListExample.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,42 @@ function ChatListExample() {
6161
if (chatListAray.length === 5) return
6262
setChatListAray([
6363
...chatListAray,
64-
{
65-
id: String(Math.random()),
66-
avatar: `data:image/png;base64,${photo(20)}`,
67-
avatarFlexible: true,
68-
statusColor: 'lightgreen',
69-
statusColorType: Math.floor((Math.random() * 100) % 2) === 1 ? 'encircle' : undefined,
70-
alt: loremIpsum({ count: 2, units: 'words' }),
71-
title: loremIpsum({ count: 2, units: 'words' }),
72-
date: new Date(),
73-
subtitle: loremIpsum({ count: 1, units: 'sentences' }),
74-
unread: Math.floor((Math.random() * 10) % 3),
75-
muted: Math.floor((Math.random() * 10) % 2) === 1,
76-
showMute: Math.floor((Math.random() * 10) % 2) === 1,
77-
showVideoCall: Math.floor((Math.random() * 10) % 2) === 1,
78-
customStatusComponents: [Test],
79-
},
64+
getRandomChat(),
8065
])
8166
}, [chatListAray])
8267

68+
const getRandomChat: any = (nested = true) => {
69+
return {
70+
id: String(Math.random()),
71+
avatar: `data:image/png;base64,${photo(20)}`,
72+
avatarFlexible: true,
73+
statusColor: 'lightgreen',
74+
statusColorType: Math.floor((Math.random() * 100) % 2) === 1 ? 'encircle' : undefined,
75+
alt: loremIpsum({ count: 2, units: 'words' }),
76+
title: loremIpsum({ count: 2, units: 'words' }),
77+
date: new Date(),
78+
subtitle: loremIpsum({ count: 1, units: 'sentences' }),
79+
unread: Math.floor((Math.random() * 10) % 3),
80+
muted: Math.floor((Math.random() * 10) % 2) === 1,
81+
showMute: Math.floor((Math.random() * 10) % 2) === 1,
82+
showVideoCall: Math.floor((Math.random() * 10) % 2) === 1,
83+
customStatusComponents: [Test],
84+
subList: nested && [getRandomLiteChat(), getRandomLiteChat()]
85+
};
86+
}
87+
88+
const getRandomLiteChat: any = () => {
89+
return {
90+
id: String(Math.random()),
91+
avatar: `data:image/png;base64,${photo(20)}`,
92+
avatarFlexible: true,
93+
title: loremIpsum({ count: 2, units: 'words' }),
94+
subtitle: loremIpsum({ count: 1, units: 'sentences' }),
95+
date: new Date(),
96+
unread: Math.floor((Math.random() * 10) % 3),
97+
};
98+
};
99+
83100
return (
84101
<div className='chat-list'>
85102
<SideBar

src/ChatItem/ChatItem.css

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
justify-content: center;
3333
display: flex;
3434
align-items: center;
35+
flex-direction: column;
3536
}
3637

3738
.rce-citem-status {
@@ -129,6 +130,7 @@
129130
justify-content: center;
130131
display: flex;
131132
border-radius: 100%;
133+
background: red;
132134
}
133135

134136
.rce-citem-body--bottom-status-icon {
@@ -184,3 +186,54 @@
184186
height: 18px;
185187
fill: #575757;
186188
}
189+
190+
.rce-container-citem.subitem .rce-citem {
191+
height: 40px;
192+
padding-left: 30px;
193+
}
194+
195+
.rce-container-citem.subitem .rce-citem-body--top .rce-citem-body--top-title {
196+
font-size: 12px !important;
197+
}
198+
199+
.rce-container-citem.subitem .rce-citem-body--top .rce-citem-body--top-time {
200+
font-size: 10px !important;
201+
}
202+
203+
.rce-container-citem.subitem .rce-citem-body--bottom * {
204+
font-size: 12px !important;
205+
}
206+
207+
.rce-container-citem.subitem .rce-citem-body--bottom-status span {
208+
width: 14px;
209+
height: 14px;
210+
font-size: 10px !important;
211+
color: white;
212+
font-weight: bold;
213+
text-align: center;
214+
align-items: center;
215+
justify-content: center;
216+
display: flex;
217+
border-radius: 100%;
218+
background: red;
219+
}
220+
221+
.rce-container-citem.subitem .rce-citem-body--bottom {
222+
margin-top: unset;
223+
}
224+
225+
.rce-citem-expand-button {
226+
background: transparent;
227+
border: none;
228+
cursor: pointer;
229+
padding: 2px 15px;
230+
color: teal;
231+
}
232+
233+
.rce-citem-expand-button:hover {
234+
background-color: #eee;
235+
}
236+
237+
.rce-sublist-container {
238+
position: relative;
239+
}

src/ChatItem/ChatItem.tsx

Lines changed: 92 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import classNames from 'classnames'
99

1010
import { MdVideoCall, MdVolumeOff, MdVolumeUp } from 'react-icons/md'
1111
import { IChatItemProps } from '../type'
12+
import { FaArrowDown, FaArrowUp } from 'react-icons/fa'
1213

1314
const ChatItem: React.FC<IChatItemProps> = ({
1415
avatarFlexible = false,
@@ -65,86 +66,99 @@ const ChatItem: React.FC<IChatItemProps> = ({
6566
if (onDrag) setOnDrag(false)
6667
}
6768

68-
return (
69-
<div
70-
key={props.id as Key}
71-
className={classNames('rce-container-citem', props.className)}
72-
onClick={handleOnClick}
73-
onContextMenu={props.onContextMenu}
74-
>
75-
<div className='rce-citem' onDragOver={onDragOver} onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDrop}>
76-
{!!props.onDragComponent && onDrag && props.onDragComponent(props.id)}
77-
{((onDrag && !props.onDragComponent) || !onDrag) && [
78-
<div
79-
key={'avatar'}
80-
className={classNames('rce-citem-avatar', { 'rce-citem-status-encircle': statusColorType === 'encircle' })}
81-
>
82-
<Avatar
83-
src={props.avatar}
84-
alt={props.alt}
85-
className={statusColorType === 'encircle' ? 'rce-citem-avatar-encircle-status' : ''}
86-
size='large'
87-
letterItem={props.letterItem}
88-
sideElement={
89-
props.statusColor ? (
90-
<span
91-
className='rce-citem-status'
92-
style={
93-
statusColorType === 'encircle'
94-
? {
95-
border: `solid 2px ${props.statusColor}`,
96-
}
97-
: {
98-
backgroundColor: props.statusColor,
99-
}
100-
}
101-
>
102-
{props.statusText}
103-
</span>
104-
) : (
105-
<></>
106-
)
107-
}
108-
onError={onAvatarError}
109-
lazyLoadingImage={lazyLoadingImage}
110-
type={classNames('circle', { 'flexible': avatarFlexible })}
111-
/>
112-
</div>,
113-
<div key={'rce-citem-body'} className='rce-citem-body'>
114-
<div className='rce-citem-body--top'>
115-
<div className='rce-citem-body--top-title'>{props.title}</div>
116-
<div className='rce-citem-body--top-time'>{date && (props.dateString || format(date))}</div>
117-
</div>
69+
const onExpandItem = (e: React.MouseEvent, id: string | number) => {
70+
e.preventDefault();
71+
e.stopPropagation();
72+
if (props.onExpandItem instanceof Function) props.onExpandItem(id);
73+
}
11874

119-
<div className='rce-citem-body--bottom'>
120-
<div className='rce-citem-body--bottom-title'>{props.subtitle}</div>
121-
<div className='rce-citem-body--bottom-tools' onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
122-
{props.showMute && (
123-
<div className='rce-citem-body--bottom-tools-item' onClick={props.onClickMute}>
124-
{props.muted === true && <MdVolumeOff />}
125-
{props.muted === false && <MdVolumeUp />}
126-
</div>
127-
)}
128-
{props.showVideoCall && (
129-
<div className='rce-citem-body--bottom-tools-item' onClick={props.onClickVideoCall}>
130-
<MdVideoCall />
131-
</div>
132-
)}
133-
</div>
134-
<div className='rce-citem-body--bottom-tools-item-hidden-hover'>
135-
{props.showMute && props.muted && (
136-
<div className='rce-citem-body--bottom-tools-item'>
137-
<MdVolumeOff />
138-
</div>
139-
)}
140-
</div>
141-
<div className='rce-citem-body--bottom-status'>{unread && unread > 0 ? <span>{unread}</span> : null}</div>
142-
{props.customStatusComponents !== undefined ? props.customStatusComponents.map(Item => <Item />) : null}
75+
return (
76+
<>
77+
<div
78+
key={props.id as Key}
79+
className={classNames('rce-container-citem', props.className)}
80+
onClick={handleOnClick}
81+
onContextMenu={props.onContextMenu}
82+
>
83+
<div className='rce-citem' onDragOver={onDragOver} onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDrop}>
84+
{!!props.onDragComponent && onDrag && props.onDragComponent(props.id)}
85+
{((onDrag && !props.onDragComponent) || !onDrag) && [
86+
<div
87+
key={'avatar'}
88+
className={classNames('rce-citem-avatar', { 'rce-citem-status-encircle': statusColorType === 'encircle' })}
89+
>
90+
<Avatar
91+
src={props.avatar}
92+
alt={props.alt}
93+
className={statusColorType === 'encircle' ? 'rce-citem-avatar-encircle-status' : ''}
94+
size={props.avatarSize || 'large'}
95+
letterItem={props.letterItem}
96+
sideElement={
97+
props.statusColor ? (
98+
<span
99+
className='rce-citem-status'
100+
style={
101+
statusColorType === 'encircle'
102+
? {
103+
border: `solid 2px ${props.statusColor}`,
104+
}
105+
: {
106+
backgroundColor: props.statusColor,
107+
}
108+
}
109+
>
110+
{props.statusText}
111+
</span>
112+
) : (
113+
<></>
114+
)
115+
}
116+
onError={onAvatarError}
117+
lazyLoadingImage={lazyLoadingImage}
118+
type={classNames('circle', { 'flexible': avatarFlexible })}
119+
/>
120+
{props.subList && props.subList.length > 0 && (
121+
<button className='rce-citem-expand-button' onClick={(e) => onExpandItem(e, props.id)}>
122+
{props.expanded ? <FaArrowUp /> : <FaArrowDown />}
123+
</button>
124+
)}
125+
</div>,
126+
<div key={'rce-citem-body'} className='rce-citem-body'>
127+
<div className='rce-citem-body--top'>
128+
<div className='rce-citem-body--top-title'>{props.title}</div>
129+
<div className='rce-citem-body--top-time'>{date && (props.dateString || format(date))}</div>
130+
</div>
131+
132+
<div className='rce-citem-body--bottom'>
133+
<div className='rce-citem-body--bottom-title'>{props.subtitle}</div>
134+
<div className='rce-citem-body--bottom-tools' onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
135+
{props.showMute && (
136+
<div className='rce-citem-body--bottom-tools-item' onClick={props.onClickMute}>
137+
{props.muted === true && <MdVolumeOff />}
138+
{props.muted === false && <MdVolumeUp />}
139+
</div>
140+
)}
141+
{props.showVideoCall && (
142+
<div className='rce-citem-body--bottom-tools-item' onClick={props.onClickVideoCall}>
143+
<MdVideoCall />
144+
</div>
145+
)}
146+
</div>
147+
<div className='rce-citem-body--bottom-tools-item-hidden-hover'>
148+
{props.showMute && props.muted && (
149+
<div className='rce-citem-body--bottom-tools-item'>
150+
<MdVolumeOff />
151+
</div>
152+
)}
153+
</div>
154+
<div className='rce-citem-body--bottom-status'>{unread && unread > 0 ? <span>{unread}</span> : null}</div>
155+
{props.customStatusComponents !== undefined ? props.customStatusComponents.map(Item => <Item />) : null}
156+
</div>
157+
</div>,
158+
]}
143159
</div>
144-
</div>,
145-
]}
146-
</div>
147-
</div>
160+
</div>
161+
</>
148162
)
149163
}
150164

src/ChatList/ChatList.tsx

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,57 @@ const ChatList: React.FC<IChatListProps> = props => {
3030
props.onDragLeave?.(e, id)
3131
}
3232

33+
const onExpand: ChatListEvent = (item, index, event) => {
34+
if (props.onExpand instanceof Function) props.onExpand(item, index, event)
35+
}
36+
3337
return (
3438
<div className={classNames('rce-container-clist', props.className)}>
3539
{props.dataSource.map((x, i: number) => (
36-
<ChatItem
37-
{...x}
38-
key={i as Key}
39-
lazyLoadingImage={props.lazyLoadingImage}
40-
onAvatarError={(e: React.MouseEvent<HTMLElement>) => onAvatarError(x, i, e)}
41-
onContextMenu={(e: React.MouseEvent<HTMLElement>) => onContextMenu(x, i, e)}
42-
onClick={(e: React.MouseEvent<HTMLElement>) => onClick(x, i, e)}
43-
onClickMute={(e: React.MouseEvent<HTMLElement>) => props.onClickMute?.(x, i, e)}
44-
onClickVideoCall={(e: React.MouseEvent<HTMLElement>) => props.onClickVideoCall?.(x, i, e)}
45-
onDragOver={props?.onDragOver}
46-
onDragEnter={props?.onDragEnter}
47-
onDrop={props.onDrop}
48-
onDragLeave={onDragLeaveMW}
49-
onDragComponent={props.onDragComponent}
50-
setDragStates={setDragStates}
51-
/>
40+
<>
41+
<ChatItem
42+
{...x}
43+
key={i as Key}
44+
lazyLoadingImage={props.lazyLoadingImage}
45+
onAvatarError={(e: React.MouseEvent<HTMLElement>) => onAvatarError(x, i, e)}
46+
onContextMenu={(e: React.MouseEvent<HTMLElement>) => onContextMenu(x, i, e)}
47+
onClick={(e: React.MouseEvent<HTMLElement>) => onClick(x, i, e)}
48+
onClickMute={(e: React.MouseEvent<HTMLElement>) => props.onClickMute?.(x, i, e)}
49+
onClickVideoCall={(e: React.MouseEvent<HTMLElement>) => props.onClickVideoCall?.(x, i, e)}
50+
onDragOver={props?.onDragOver}
51+
onDragEnter={props?.onDragEnter}
52+
onDrop={props.onDrop}
53+
onDragLeave={onDragLeaveMW}
54+
onDragComponent={props.onDragComponent}
55+
setDragStates={setDragStates}
56+
onExpandItem={(e: React.MouseEvent<HTMLElement>) => onExpand(x, i, e)}
57+
expanded={x.expanded}
58+
/>
59+
{x.subList && x.subList.length > 0 && (
60+
<>
61+
{x.expanded && x.subList.map((sub, j) => (
62+
<ChatItem
63+
{...sub}
64+
className='subitem'
65+
avatarSize='xsmall'
66+
key={`${i}-${j}`}
67+
lazyLoadingImage={props.lazyLoadingImage}
68+
onAvatarError={(e: React.MouseEvent<HTMLElement>) => onAvatarError(sub, j, e)}
69+
onContextMenu={(e: React.MouseEvent<HTMLElement>) => onContextMenu(sub, j, e)}
70+
onClick={(e: React.MouseEvent<HTMLElement>) => onClick(sub, j, e)}
71+
onClickMute={(e: React.MouseEvent<HTMLElement>) => props.onClickMute?.(sub, j, e)}
72+
onClickVideoCall={(e: React.MouseEvent<HTMLElement>) => props.onClickVideoCall?.(sub, j, e)}
73+
onDragOver={props?.onDragOver}
74+
onDragEnter={props?.onDragEnter}
75+
onDrop={props.onDrop}
76+
onDragLeave={onDragLeaveMW}
77+
onDragComponent={props.onDragComponent}
78+
setDragStates={setDragStates}
79+
/>
80+
))}
81+
</>
82+
)}
83+
</>
5284
))}
5385
</div>
5486
)

0 commit comments

Comments
 (0)