Skip to content

Commit f37f0a9

Browse files
authored
feat: add pin icon to the ChannelListItemUI (#3163)
1 parent 88ef71e commit f37f0a9

5 files changed

Lines changed: 74 additions & 6 deletions

File tree

src/components/ChannelList/styling/ChannelList.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@
4949
flex: 1;
5050
min-height: 0;
5151
overflow: hidden;
52+
padding-bottom: calc(var(--str-chat__space-8) + var(--str-chat__space-2));
5253

5354
.str-chat__channel-list-inner__main {
5455
height: 100%;
5556
overflow-y: auto;
5657
scrollbar-gutter: stable both-edges;
5758
scrollbar-width: thin;
58-
padding-bottom: calc(var(--str-chat__space-8) + var(--str-chat__space-2));
5959

6060
.str-chat__channel-list-empty {
6161
height: 100%;

src/components/ChannelListItem/ChannelListItem.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
useChatContext,
1818
useComponentContext,
1919
} from '../../context';
20+
import { useChannelMembershipState } from '../ChannelList';
2021

2122
export type ChannelListItemUIProps = ChannelListItemProps & {
2223
/** Image of Channel to display */
@@ -33,6 +34,8 @@ export type ChannelListItemUIProps = ChannelListItemProps & {
3334
messageDeliveryStatus?: MessageDeliveryStatus;
3435
/** Whether the channel is muted by the current user */
3536
muted?: boolean;
37+
/** Whether the channel is pinned by the current user */
38+
pinned?: boolean;
3639
/** Number of unread Messages */
3740
unread?: number;
3841
};
@@ -88,6 +91,7 @@ export const ChannelListItem = (props: ChannelListItemProps) => {
8891
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
8992
channel,
9093
});
94+
const membership = useChannelMembershipState(channel);
9195

9296
const [lastMessage, setLastMessage] = useState<LocalMessage>(
9397
channel.state.messages[channel.state.messages.length - 1],
@@ -203,6 +207,7 @@ export const ChannelListItem = (props: ChannelListItemProps) => {
203207
latestMessagePreview={latestMessagePreview}
204208
messageDeliveryStatus={messageDeliveryStatus}
205209
muted={muted}
210+
pinned={!!membership.pinned_at}
206211
setActiveChannel={setActiveChannel}
207212
unread={unread}
208213
/>

src/components/ChannelListItem/ChannelListItemUI.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ChannelListItemTimestamp } from './ChannelListItemTimestamp';
66

77
import { ChannelAvatar as DefaultChannelAvatar } from '../Avatar';
88
import { Badge } from '../Badge';
9-
import { IconMute } from '../Icons';
9+
import { IconMute, IconPin } from '../Icons';
1010
import { useComponentContext, useTranslationContext } from '../../context';
1111
import type { ChannelListItemUIProps } from './ChannelListItem';
1212
import { SummarizedMessagePreview } from '../SummarizedMessagePreview';
@@ -23,6 +23,7 @@ const UnMemoizedChannelListItemUI = (props: ChannelListItemUIProps) => {
2323
messageDeliveryStatus,
2424
muted,
2525
onSelect: customOnSelectChannel,
26+
pinned,
2627
setActiveChannel,
2728
unread,
2829
watchers,
@@ -60,10 +61,12 @@ const UnMemoizedChannelListItemUI = (props: ChannelListItemUIProps) => {
6061
aria-selected={active}
6162
className={clsx(
6263
'str-chat__channel-list-item',
63-
typeof unread === 'number' &&
64-
unread > 0 &&
65-
'str-chat__channel-list-item--unread',
66-
muted && 'str-chat__channel-list-item--muted',
64+
{
65+
'str-chat__channel-list-item--muted': muted,
66+
'str-chat__channel-list-item--pinned': pinned,
67+
'str-chat__channel-list-item--unread':
68+
typeof unread === 'number' && unread > 0,
69+
},
6770
customClassName,
6871
)}
6972
data-testid='channel-list-item-button'
@@ -81,6 +84,7 @@ const UnMemoizedChannelListItemUI = (props: ChannelListItemUIProps) => {
8184
<div className='str-chat__channel-list-item-data__first-row'>
8285
<div className='str-chat__channel-list-item-data__title'>
8386
<span>{displayTitle || 'N/A'}</span>
87+
{pinned && <IconPin />}
8488
{muted && <IconMute />}
8589
</div>
8690
<div className='str-chat__channel-list-item-data__timestamp-and-badge'>

src/components/ChannelListItem/__tests__/ChannelListItem.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const PreviewUIComponent = (props: ChannelListItemUIProps) => (
4949
<div data-testid='last-event-message'>
5050
{props.lastMessage ? props.lastMessage.text : EMPTY_CHANNEL_PREVIEW_TEXT}
5151
</div>
52+
<div data-testid='pinned'>{String(!!props.pinned)}</div>
5253
</>
5354
);
5455
const PreviewUIComponentWithLatestMessagePreview = (props: ChannelListItemUIProps) => (
@@ -674,6 +675,39 @@ describe('ChannelPreview', () => {
674675
});
675676
});
676677

678+
describe('pinned', () => {
679+
it('should pass pinned=false when membership has no pinned_at', async () => {
680+
renderComponent(
681+
{
682+
activeChannel: c1,
683+
channel: c0,
684+
},
685+
render,
686+
);
687+
await waitFor(() => {
688+
expect(screen.getByTestId('pinned')).toHaveTextContent('false');
689+
});
690+
});
691+
692+
it('should pass pinned=true when membership has pinned_at', async () => {
693+
c0.state.membership = fromPartial({
694+
...c0.state.membership,
695+
pinned_at: '2024-01-01T00:00:00Z',
696+
});
697+
698+
renderComponent(
699+
{
700+
activeChannel: c1,
701+
channel: c0,
702+
},
703+
render,
704+
);
705+
await waitFor(() => {
706+
expect(screen.getByTestId('pinned')).toHaveTextContent('true');
707+
});
708+
});
709+
});
710+
677711
describe('user.updated', () => {
678712
const renderComponent = async ({
679713
channel,

src/components/ChannelListItem/__tests__/ChannelListItemUI.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,29 @@ describe('ChannelPreviewMessenger', () => {
127127
fireEvent.click(previewButton);
128128
expect(onSelect).toHaveBeenCalledTimes(1);
129129
});
130+
131+
describe('pinned', () => {
132+
it('should not add pinned class or render pin icon when not pinned', () => {
133+
const { container } = render(renderComponent({ pinned: false }));
134+
const button = screen.getByTestId(PREVIEW_TEST_ID);
135+
expect(button).not.toHaveClass('str-chat__channel-list-item--pinned');
136+
expect(container.querySelector('.str-chat__icon--pin')).not.toBeInTheDocument();
137+
});
138+
139+
it('should add pinned class and render pin icon when pinned', () => {
140+
const { container } = render(renderComponent({ pinned: true }));
141+
const button = screen.getByTestId(PREVIEW_TEST_ID);
142+
expect(button).toHaveClass('str-chat__channel-list-item--pinned');
143+
expect(container.querySelector('.str-chat__icon--pin')).toBeInTheDocument();
144+
});
145+
146+
it('should render both pin and mute icons when pinned and muted', () => {
147+
const { container } = render(renderComponent({ muted: true, pinned: true }));
148+
const button = screen.getByTestId(PREVIEW_TEST_ID);
149+
expect(button).toHaveClass('str-chat__channel-list-item--pinned');
150+
expect(button).toHaveClass('str-chat__channel-list-item--muted');
151+
expect(container.querySelector('.str-chat__icon--pin')).toBeInTheDocument();
152+
expect(container.querySelector('.str-chat__icon--mute')).toBeInTheDocument();
153+
});
154+
});
130155
});

0 commit comments

Comments
 (0)