Skip to content

Commit

Permalink
feature: Support reactions in the SuperGroupChannel (#1029)
Browse files Browse the repository at this point in the history
### ChangeLog & Feature
* Support reaction feature in the SuperGroupChannel

### Internal change
* modify the `getIsReactionEnabled`
  • Loading branch information
HoonBaek authored Mar 27, 2024
1 parent 6a5f169 commit becee89
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 155 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
},
"dependencies": {
"@sendbird/chat": "^4.11.0",
"@sendbird/react-uikit-message-template-view": "0.0.1-alpha.65",
"@sendbird/uikit-tools": "0.0.1-alpha.65",
"@sendbird/react-uikit-message-template-view": "0.0.1-alpha.68",
"@sendbird/uikit-tools": "0.0.1-alpha.68",
"css-vars-ponyfill": "^2.3.2",
"date-fns": "^2.16.1",
"dompurify": "^3.0.1"
Expand Down
1 change: 1 addition & 0 deletions src/lib/Sendbird.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ const SendbirdSDK = ({
enableTypingIndicator: configs.groupChannel.channel.enableTypingIndicator,
enableDocument: configs.groupChannel.channel.input.enableDocument,
enableReactions: sdkInitialized && configsWithAppAttr(sdk).groupChannel.channel.enableReactions,
enableReactionsSupergroup: sdkInitialized && configsWithAppAttr(sdk).groupChannel.channel.enableReactionsSupergroup,
replyType: configs.groupChannel.channel.replyType,
threadReplySelectType: getCaseResolvedThreadReplySelectType(configs.groupChannel.channel.threadReplySelectType).lowerCase,
typingIndicatorTypes: configs.groupChannel.channel.typingIndicatorTypes,
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export interface SendBirdStateConfig {
enableTypingIndicator: SBUConfig['groupChannel']['channel']['enableTypingIndicator'];
enableDocument: SBUConfig['groupChannel']['channel']['input']['enableDocument'];
enableReactions: SBUConfig['groupChannel']['channel']['enableReactions'];
enableReactionsSupergroup: SBUConfig['groupChannel']['channel']['enableReactionsSupergroup'];
replyType: SBUConfig['groupChannel']['channel']['replyType'];
threadReplySelectType: SBUConfig['groupChannel']['channel']['threadReplySelectType'];
typingIndicatorTypes: SBUConfig['groupChannel']['channel']['typingIndicatorTypes'];
Expand Down
7 changes: 2 additions & 5 deletions src/modules/Channel/context/ChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,9 @@ const ChannelProvider: React.FC<ChannelContextProps> = (props: ChannelContextPro
typingMembers,
} = messagesStore;

const isSuper = currentGroupChannel?.isSuper || false;
const isBroadcast = currentGroupChannel?.isBroadcast || false;
const usingReaction = getIsReactionEnabled({
isBroadcast,
isSuper,
globalLevel: config?.isReactionEnabled,
channel: currentGroupChannel,
config,
moduleLevel: isReactionEnabled,
});

Expand Down
8 changes: 5 additions & 3 deletions src/modules/GroupChannel/context/GroupChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import PUBSUB_TOPICS, { PubSubSendMessagePayload } from '../../../lib/pubSub/top
import { PubSubTypes } from '../../../lib/pubSub';
import { useMessageActions } from './hooks/useMessageActions';
import { usePreventDuplicateRequest } from './hooks/usePreventDuplicateRequest';
import { getIsReactionEnabled } from '../../../utils/getIsReactionEnabled';

type OnBeforeHandler<T> = (params: T) => T | Promise<T>;
type MessageListQueryParamsType = Omit<MessageCollectionParams, 'filter'> & MessageFilterParams;
Expand Down Expand Up @@ -162,9 +163,10 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
if (replyType === 'NONE') return ChatReplyType.NONE;
return ChatReplyType.ONLY_REPLY_TO_CHANNEL;
});
const isReactionEnabled = useIIFE(() => {
if (!currentChannel || currentChannel.isSuper || currentChannel.isBroadcast || currentChannel.isEphemeral) return false;
return moduleReactionEnabled ?? config.groupChannel.enableReactions;
const isReactionEnabled = getIsReactionEnabled({
channel: currentChannel,
config,
moduleLevel: moduleReactionEnabled,
});
const nicknamesMap = useMemo(
() => new Map((currentChannel?.members ?? []).map(({ userId, nickname }) => [userId, nickname])),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ export default function ParentMessageInfoItem({
// Emoji reactions
const isReactionActivated = isReactionEnabled
&& replyType === 'THREAD'
&& !currentChannel?.isSuper
&& !currentChannel?.isBroadcast
&& message?.reactions?.length > 0;

const tokens = useMemo(() => {
Expand Down
6 changes: 3 additions & 3 deletions src/modules/Thread/components/ParentMessageInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export default function ParentMessageInfo({
const [supposedHover, setSupposedHover] = useState(false);
const [showFileViewer, setShowFileViewer] = useState(false);
const usingReaction = getIsReactionEnabled({
globalLevel: isReactionEnabled,
isSuper: currentChannel.isSuper,
isBroadcast: currentChannel.isBroadcast,
channel: currentChannel,
config,
moduleLevel: isReactionEnabled,
});
const isByMe = userId === parentMessage.sender.userId;

Expand Down
6 changes: 3 additions & 3 deletions src/modules/Thread/components/ThreadList/ThreadListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export default function ThreadListItem({
const [showRemove, setShowRemove] = useState(false);
const [showFileViewer, setShowFileViewer] = useState(false);
const usingReaction = getIsReactionEnabled({
globalLevel: isReactionEnabled,
isSuper: currentChannel.isSuper,
isBroadcast: currentChannel.isBroadcast,
channel: currentChannel,
config,
moduleLevel: isReactionEnabled,
});

// Move to message
Expand Down
42 changes: 28 additions & 14 deletions src/ui/EmojiReactions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './index.scss';
import React, { ReactElement, useRef, useState } from 'react';
import type { Emoji, EmojiContainer } from '@sendbird/chat';
import type { Emoji, EmojiContainer, User } from '@sendbird/chat';
import type { Reaction } from '@sendbird/chat/message';
import type { GroupChannel } from '@sendbird/chat/groupChannel';

Expand All @@ -17,7 +17,8 @@ import ReactionItem from './ReactionItem';
import { useMediaQueryContext } from '../../lib/MediaQueryContext';
import { AddReactionBadgeItem } from './AddReactionBadgeItem';
import { MobileEmojisBottomSheet } from '../MobileMenu/MobileEmojisBottomSheet';
import { User } from '@sendbird/chat';
import useSendbirdStateContext from '../../hooks/useSendbirdStateContext';
import { getIsReactionEnabled } from '../../utils/getIsReactionEnabled';

export interface EmojiReactionsProps {
className?: string | Array<string>;
Expand All @@ -44,6 +45,17 @@ const EmojiReactions = ({
toggleReaction,
onPressUserProfile,
}: EmojiReactionsProps): ReactElement => {
let showTheReactedMembers = false;
try {
const { config } = useSendbirdStateContext();
showTheReactedMembers = getIsReactionEnabled({
channel,
config,
});
} catch (err) {
// TODO: Handle error
}

const { isMobile } = useMediaQueryContext();
const addReactionRef = useRef(null);
const [showEmojiList, setShowEmojiList] = useState(false);
Expand Down Expand Up @@ -157,18 +169,20 @@ const EmojiReactions = ({
toggleReaction={toggleReaction}
/>
)}
{(isMobile && selectedEmojiKey && channel !== null) && (
<ReactedMembersBottomSheet
message={message}
channel={channel}
emojiKey={selectedEmojiKey}
hideMenu={() => {
setSelectedEmojiKey('');
}}
emojiContainer={emojiContainer}
onPressUserProfileHandler={onPressUserProfile}
/>
)}
{
(isMobile && selectedEmojiKey && channel !== null && showTheReactedMembers) && (
<ReactedMembersBottomSheet
message={message}
channel={channel}
emojiKey={selectedEmojiKey}
hideMenu={() => {
setSelectedEmojiKey('');
}}
emojiContainer={emojiContainer}
onPressUserProfileHandler={onPressUserProfile}
/>
)
}
</div>
);
};
Expand Down
168 changes: 67 additions & 101 deletions src/utils/__tests__/getIsReactionEnabled.spec.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,90 @@
import { getIsReactionEnabled } from '../getIsReactionEnabled';
import type { GroupChannel } from '@sendbird/chat/groupChannel';

describe('Global-utils/getIsReactionEnabled', () => {
it('should enable as a default', () => {
expect(getIsReactionEnabled({})).toBeTrue();
import type { SendBirdStateConfig } from '../../lib/types';
import { getIsReactionEnabled } from '../getIsReactionEnabled';

expect(getIsReactionEnabled({
globalLevel: true,
})).toBeTrue();
expect(getIsReactionEnabled({
moduleLevel: true,
})).toBeTrue();
const normalGroupChannel = (props?) => ({
isBroadcast: false,
isEphemeral: false,
isSuper: false,
...props,
} as GroupChannel);
const normalConfigs = (props?, groupChannelProps?) => ({
groupChannel: {
enableReactions: true,
enableReactionsSupergroup: false,
...groupChannelProps,
},
...props,
} as SendBirdStateConfig);

expect(getIsReactionEnabled({
globalLevel: true,
moduleLevel: true,
})).toBeTrue();
describe('Global-utils/getIsReactionEnabled', () => {
it('should prioritize the moduleLevel than global config', () => {
const moduleLevel = true;
expect(getIsReactionEnabled({
channel: normalGroupChannel(),
config: normalConfigs(),
moduleLevel,
})).toBe(moduleLevel);
const moduleLevel2 = false;
expect(getIsReactionEnabled({
channel: normalGroupChannel(),
config: normalConfigs(),
moduleLevel: moduleLevel2,
})).toBe(moduleLevel2);
});

it('should disable if set values are false', () => {
expect(getIsReactionEnabled({
globalLevel: false,
})).toBeFalse();
expect(getIsReactionEnabled({
moduleLevel: false,
})).toBeFalse();

it('should prioritize the isSuper than moduleLevel', () => {
const isSuper = true;
expect(getIsReactionEnabled({
globalLevel: false,
moduleLevel: false,
})).toBeFalse();
});

it('should have higher priority to the moduleLevel than globalLevel', () => {
channel: normalGroupChannel({ isSuper }),
config: normalConfigs(),
moduleLevel: true,
})).toBe(false);
expect(getIsReactionEnabled({
globalLevel: true,
channel: normalGroupChannel({ isSuper }),
config: normalConfigs(),
moduleLevel: false,
})).toBeFalse();
expect(getIsReactionEnabled({
globalLevel: false,
moduleLevel: true,
})).toBeTrue();
})).toBe(false);
});

it('should disable in the special type channels', () => {
expect(getIsReactionEnabled({
globalLevel: true,
isBroadcast: true,
})).toBeFalse();
expect(getIsReactionEnabled({
globalLevel: true,
isSuper: true,
})).toBeFalse();
expect(getIsReactionEnabled({
globalLevel: true,
isBroadcast: true,
isSuper: true,
})).toBeFalse();

expect(getIsReactionEnabled({
moduleLevel: true,
isBroadcast: true,
})).toBeFalse();
expect(getIsReactionEnabled({
moduleLevel: true,
isSuper: true,
})).toBeFalse();
expect(getIsReactionEnabled({
moduleLevel: true,
isBroadcast: true,
isSuper: true,
})).toBeFalse();

it('should prioritize moduleLevel than enableReactionsSupergroup', () => {
const isSuper = true;
expect(getIsReactionEnabled({
globalLevel: true,
channel: normalGroupChannel({ isSuper }),
config: normalConfigs({}, { enableReactionsSupergroup: true }),
moduleLevel: true,
isBroadcast: true,
})).toBeFalse();
})).toBe(true);
expect(getIsReactionEnabled({
globalLevel: true,
moduleLevel: true,
isSuper: true,
})).toBeFalse();
expect(getIsReactionEnabled({
globalLevel: true,
moduleLevel: true,
isBroadcast: true,
isSuper: true,
})).toBeFalse();
channel: normalGroupChannel({ isSuper }),
config: normalConfigs({}, { enableReactionsSupergroup: true }),
moduleLevel: false,
})).toBe(false);
});

it('should disable if only one special channel type is true', () => {
expect(getIsReactionEnabled({
globalLevel: true,
moduleLevel: true,
isBroadcast: false,
isSuper: true,
})).toBeFalse();
it('should be false when it is broadcast or ephemeral channel', () => {
const isBroadcast = true;
expect(getIsReactionEnabled({
globalLevel: true,
channel: normalGroupChannel({ isBroadcast }),
config: normalConfigs(),
moduleLevel: true,
isBroadcast: true,
isSuper: false,
})).toBeFalse();
})).toBe(false);
expect(getIsReactionEnabled({
globalLevel: true,
isBroadcast: false,
isSuper: true,
})).toBeFalse();
channel: normalGroupChannel({ isBroadcast }),
config: normalConfigs(),
moduleLevel: false,
})).toBe(false);

const isEphemeral = true;
expect(getIsReactionEnabled({
channel: normalGroupChannel({ isEphemeral }),
config: normalConfigs(),
moduleLevel: true,
isBroadcast: true,
isSuper: false,
})).toBeFalse();
})).toBe(false);
expect(getIsReactionEnabled({
isBroadcast: false,
isSuper: true,
})).toBeFalse();
expect(getIsReactionEnabled({
isBroadcast: true,
isSuper: false,
})).toBeFalse();
channel: normalGroupChannel({ isEphemeral }),
config: normalConfigs(),
moduleLevel: false,
})).toBe(false);
});
});
19 changes: 12 additions & 7 deletions src/utils/getIsReactionEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
* related to enabling emoji reaction feature.
*/

import type { GroupChannel } from '@sendbird/chat/groupChannel';
import type { SendBirdStateConfig } from '../lib/types';

export interface IsReactionEnabledProps {
isBroadcast?: boolean;
isSuper?: boolean;
globalLevel?: boolean;
channel: GroupChannel;
config: SendBirdStateConfig;
moduleLevel?: boolean;
}

export function getIsReactionEnabled({
isBroadcast = false,
isSuper = false,
globalLevel = true,
channel,
config,
moduleLevel,
}: IsReactionEnabledProps): boolean {
return !(isBroadcast || isSuper) && (moduleLevel ?? globalLevel);
if (!channel || channel.isBroadcast || channel.isEphemeral) {
return false;
}
if (channel.isSuper) return moduleLevel && config.groupChannel.enableReactionsSupergroup;
return moduleLevel ?? config.groupChannel.enableReactions;
}
Loading

0 comments on commit becee89

Please sign in to comment.