Skip to content

Commit bcfbdb3

Browse files
authored
fix: Improve the Channel Settings Accordion Stability (#1040)
[CLNP-2679](https://sendbird.atlassian.net/browse/CLNP-2679) ### ChangeLog & Fix * Add a logger to the `GroupChannelProvider` for failing get channel * Reduce the `OGTag` height in the mobile layout * Prevent force refreshing of the `ChannelSettings` * Keep context menu when failing the member operations (register/unregister operator, mute/unmute) * Keep profile image during member operations on the `MembersModal`
1 parent 9ea1c5b commit bcfbdb3

File tree

10 files changed

+173
-137
lines changed

10 files changed

+173
-137
lines changed

.storybook/preview-head.html

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
<script>
2-
setTimeout(() => {
3-
// workaround to add light theme to storybook
4-
// todo: create a plugin/decorator
5-
const body = document.querySelector('body');
6-
body.classList.add('sendbird-theme--light');
7-
8-
const modalRoot = document.createElement('div');
9-
modalRoot.setAttribute('id', 'sendbird-modal-root');
10-
modalRoot.style.position = 'fixed';
11-
modalRoot.style.zIndex = '99999999999';
12-
body.appendChild(modalRoot);
13-
});
14-
</script>
1+
<script></script>
152
<!-- for disable ios zoom https://stackoverflow.com/questions/5119090/iphone-keyboard-is-changing-zoom-scale-of-my-web-site -->
163
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1,user-scalable=0"/>

src/modules/ChannelSettings/components/ModerationPanel/MemberList.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import InviteUsers from './InviteUsersModal';
1818
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
1919
import { useChannelSettingsContext } from '../../context/ChannelSettingsProvider';
2020
import { LocalizationContext } from '../../../../lib/LocalizationContext';
21-
import uuidv4 from '../../../../utils/uuid';
21+
import { noop } from '../../../../utils/utils';
2222

2323
export const MemberList = (): ReactElement => {
2424
const [members, setMembers] = useState<Array<Member>>([]);
@@ -29,7 +29,6 @@ export const MemberList = (): ReactElement => {
2929
const state = useSendbirdStateContext();
3030
const {
3131
channel,
32-
setChannelUpdateId,
3332
} = useChannelSettingsContext();
3433
const { stringSet } = useContext(LocalizationContext);
3534

@@ -58,12 +57,11 @@ export const MemberList = (): ReactElement => {
5857
memberUserListQuery.next().then((members) => {
5958
setMembers(members);
6059
setHasNext(memberUserListQuery.hasNext);
61-
setChannelUpdateId(uuidv4());
6260
});
6361
}, [channel]);
6462

6563
return (
66-
<div className="sendbird-channel-settings-member-list sendbird-accordion">
64+
<div className="sendbird-channel-settings-member-list">
6765
{
6866
members.map((member) => (
6967
<UserListItem
@@ -103,10 +101,7 @@ export const MemberList = (): ReactElement => {
103101
refreshList();
104102
closeDropdown();
105103
}}
106-
onError={() => {
107-
// FIXME: handle error later
108-
closeDropdown();
109-
}}
104+
onError={noop} // TODO: We will handle error
110105
dataSbId={`channel_setting_member_context_menu_${(
111106
member.role !== 'operator'
112107
) ? 'register_as_operator' : 'unregister_operator'}`}
@@ -127,10 +122,7 @@ export const MemberList = (): ReactElement => {
127122
refreshList();
128123
closeDropdown();
129124
}}
130-
onError={() => {
131-
// FIXME: handle error later
132-
closeDropdown();
133-
}}
125+
onError={noop} // TODO: We will handle error
134126
dataSbId={`channel_setting_member_context_menu_${member.isMuted ? 'unmute' : 'mute'}`}
135127
>
136128
{

src/modules/ChannelSettings/components/ModerationPanel/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ import MutedMemberList from './MutedMemberList';
2424

2525
import { useChannelSettingsContext } from '../../context/ChannelSettingsProvider';
2626

27-
const kFormatter = (num: number): string|number => {
27+
const kFormatter = (num: number): string | number => {
2828
return Math.abs(num) > 999
2929
? `${(Math.abs(num) / 1000).toFixed(1)}K`
3030
: num;
3131
};
3232

33-
export default function AdminPannel(): ReactElement {
33+
export default function ModerationPanel(): ReactElement {
3434
const [frozen, setFrozen] = useState(false);
3535

3636
const { stringSet } = useContext(LocalizationContext);

src/modules/GroupChannel/context/GroupChannelProvider.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
142142
const { config, stores } = useSendbirdStateContext();
143143

144144
const { sdkStore } = stores;
145-
const { markAsReadScheduler } = config;
145+
const { markAsReadScheduler, logger } = config;
146146

147147
// State
148148
const [quoteMessage, setQuoteMessage] = useState<SendableMessageType>(null);
@@ -154,7 +154,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
154154
const { scrollRef, scrollPubSub, scrollDistanceFromBottomRef, isScrollBottomReached, setIsScrollBottomReached } = useMessageListScroll(scrollBehavior);
155155
const messageInputRef = useRef(null);
156156

157-
const toggleReaction = useToggleReactionCallback(currentChannel, config.logger);
157+
const toggleReaction = useToggleReactionCallback(currentChannel, logger);
158158
const replyType = getCaseResolvedReplyType(moduleReplyType ?? config.groupChannel.replyType).upperCase;
159159
const threadReplySelectType = getCaseResolvedThreadReplySelectType(
160160
moduleThreadReplySelectType ?? config.groupChannel.threadReplySelectType,
@@ -198,7 +198,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
198198
setFetchChannelError(null);
199199
},
200200
onChannelUpdated: (channel) => setCurrentChannel(channel),
201-
logger: config.logger,
201+
logger,
202202
});
203203

204204
useOnScrollPositionChangeDetectorWithRef(scrollRef, {
@@ -263,6 +263,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
263263
} catch (error) {
264264
setCurrentChannel(null);
265265
setFetchChannelError(error);
266+
logger?.error?.('GroupChannelProvider: error when fetching channel', error);
266267
} finally {
267268
// Reset states when channel changes
268269
setQuoteMessage(null);

src/ui/Accordion/Accordion.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { type ReactElement, useMemo } from 'react';
2+
import './index.scss';
3+
4+
import Icon, { IconTypes } from '../Icon';
5+
import { useAccordionGroupContext } from './AccordionGroup';
6+
7+
export interface AccordionProps {
8+
className?: string;
9+
id: string;
10+
renderTitle?: () => ReactElement;
11+
renderContent?: () => ReactElement;
12+
renderFooter?: () => ReactElement;
13+
}
14+
15+
export const Accordion = ({
16+
className,
17+
id,
18+
renderTitle,
19+
renderContent,
20+
renderFooter,
21+
}:AccordionProps) => {
22+
const {
23+
openedListKeys,
24+
addOpenedListKey,
25+
removeOpenedListKey,
26+
} = useAccordionGroupContext();
27+
const isOpened = useMemo(() => openedListKeys.includes(id), [openedListKeys]);
28+
const handleClick = () => {
29+
if (isOpened) {
30+
removeOpenedListKey(id);
31+
} else {
32+
addOpenedListKey(id);
33+
}
34+
};
35+
36+
return (
37+
<div className={`sendbird-accordion ${className} ${isOpened ? 'sendbird-accordion--opened' : 'sendbird-accordion--closed'}`}>
38+
<div
39+
className="sendbird-accordion__panel-header"
40+
id={id}
41+
role="switch"
42+
aria-checked={false}
43+
onClick={handleClick}
44+
onKeyDown={handleClick}
45+
tabIndex={0}
46+
>
47+
{renderTitle()}
48+
<Icon
49+
type={IconTypes.CHEVRON_RIGHT}
50+
className={[
51+
'sendbird-accordion__panel-icon-right',
52+
'sendbird-accordion__panel-icon--chevron',
53+
(isOpened ? 'sendbird-accordion__panel-icon--open' : ''),
54+
].join(' ')}
55+
height="24px"
56+
width="24px"
57+
/>
58+
</div>
59+
{
60+
isOpened && (
61+
<div className="sendbird-accordion-opened-list">
62+
<div className="sendbird-accordion__list">
63+
{renderContent()}
64+
</div>
65+
{
66+
renderFooter && (
67+
<div className="sendbird-accordion__footer">
68+
{renderFooter()}
69+
</div>
70+
)
71+
}
72+
</div>
73+
)
74+
}
75+
</div>
76+
);
77+
};

src/ui/Accordion/AccordionGroup.tsx

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,72 @@
1-
// Wraps all the accordions in an accordion set
2-
// keep one accordion open at a time
3-
import React, { ReactElement, useState } from 'react';
1+
import React, { type ReactElement, createContext, useState, useContext } from 'react';
2+
import { noop } from '../../utils/utils';
43

5-
import { Provider } from './context';
4+
// # Context
5+
export interface AccordionGroupContextType {
6+
openedListKeys: Array<string>;
7+
addOpenedListKey: (key: string) => void;
8+
removeOpenedListKey: (key: string) => void;
9+
clearOpenedListKeys: () => void;
10+
allowMultipleOpen: boolean;
11+
}
12+
type AGCType = AccordionGroupContextType;
13+
export const AccordionGroupContext = createContext<AccordionGroupContextType>({
14+
openedListKeys: [],
15+
addOpenedListKey: noop,
16+
removeOpenedListKey: noop,
17+
clearOpenedListKeys: noop,
18+
allowMultipleOpen: false,
19+
});
620

7-
interface Props {
8-
children: Array<ReactElement> | ReactElement;
21+
// # Provider Component
22+
export interface AccordionGroupProps {
923
className?: string;
24+
children: ReactElement | Array<ReactElement>;
25+
allowMultipleOpen?: boolean;
1026
}
11-
12-
export default function AccordionGroup({
27+
export const AccordionGroupProvider = ({
28+
className,
1329
children,
14-
className = '',
15-
}: Props): ReactElement {
16-
const [opened, setOpened] = useState('');
30+
allowMultipleOpen = false,
31+
}: AccordionGroupProps) => {
32+
const [openedListKeys, setOpenedListKeys] = useState([]);
33+
34+
const addOpenedListKey: AGCType['addOpenedListKey'] = (key) => {
35+
setOpenedListKeys((prevList) => {
36+
if (!allowMultipleOpen) {
37+
return [key];
38+
}
39+
prevList.push(key);
40+
return prevList;
41+
});
42+
};
43+
const removeOpenedListKey: AGCType['removeOpenedListKey'] = (key) => {
44+
setOpenedListKeys((prevList) => prevList.filter((k) => k !== key));
45+
};
46+
const clearOpenedListKeys: AGCType['clearOpenedListKeys'] = () => {
47+
setOpenedListKeys([]);
48+
};
49+
1750
return (
18-
<Provider value={{ opened, setOpened }}>
19-
<div className={className}>{ children }</div>
20-
</Provider>
51+
<div className={`sendbird-accordion-group-provider ${className}`}>
52+
<AccordionGroupContext.Provider
53+
value={{
54+
openedListKeys,
55+
addOpenedListKey,
56+
removeOpenedListKey,
57+
clearOpenedListKeys,
58+
allowMultipleOpen,
59+
}}
60+
>
61+
{children}
62+
</AccordionGroupContext.Provider>
63+
</div>
2164
);
22-
}
65+
};
66+
export const useAccordionGroupContext = () => {
67+
const context = useContext(AccordionGroupContext);
68+
if (!context) throw new Error('No accordion group context available. Make sure you are rending <AccordionGroupContext />.');
69+
return context;
70+
};
71+
72+
export default AccordionGroupProvider;

src/ui/Accordion/index.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
@import '../../styles/variables';
22

3+
.sendbird-accordion.sendbird-accordion--closed {
4+
@include themed() {
5+
border-bottom: 0px;
6+
}
7+
}
38
.sendbird-accordion {
4-
padding: 8px 0px 16px 0px;
59
position: relative;
610
box-sizing: border-box;
711
@include themed() {
@@ -23,6 +27,11 @@
2327
}
2428
}
2529

30+
.sendbird-accordion__list {
31+
padding-top: 8px;
32+
padding-bottom: 16px;
33+
}
34+
2635
.sendbird-accordion__panel-header {
2736
cursor: pointer;
2837
position: relative;

0 commit comments

Comments
 (0)