Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/testing/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import postcssRtlOptions from '../../postcssRtlOptions.mjs';
export default defineConfig({
plugins: [react(), vitePluginSvgr({ include: '**/*.svg' })],
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ['legacy-js-api'],
},
},
postcss: {
plugins: [postcssRtl(postcssRtlOptions)],
},
Expand Down
2 changes: 1 addition & 1 deletion src/modules/Channel/context/ChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import useUpdateMessageCallback from './hooks/useUpdateMessageCallback';
import useResendMessageCallback from './hooks/useResendMessageCallback';
import useSendMessageCallback from './hooks/useSendMessageCallback';
import useSendFileMessageCallback from './hooks/useSendFileMessageCallback';
import useToggleReactionCallback from '../../GroupChannel/context/hooks/useToggleReactionCallback';
import useToggleReactionCallback from './hooks/useToggleReactionCallback';
import useScrollToMessage from './hooks/useScrollToMessage';
import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback';
import { getCaseResolvedReplyType, getCaseResolvedThreadReplySelectType } from '../../../lib/utils/resolvedReplyType';
Expand Down
34 changes: 17 additions & 17 deletions src/modules/Channel/context/hooks/useToggleReactionCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,38 @@ import { GroupChannel } from '@sendbird/chat/groupChannel';
import { LoggerInterface } from '../../../../lib/Logger';
import { BaseMessage } from '@sendbird/chat/message';

type UseToggleReactionCallbackOptions = {
currentGroupChannel: GroupChannel | null;
};
type UseToggleReactionCallbackParams = {
logger: LoggerInterface;
};
const LOG_PRESET = 'useToggleReactionCallback:';

export default function useToggleReactionCallback(
{ currentGroupChannel }: UseToggleReactionCallbackOptions,
{ logger }: UseToggleReactionCallbackParams,
currentChannel: GroupChannel | null,
logger?: LoggerInterface,
) {
return useCallback(
(message: BaseMessage, key: string, isReacted: boolean) => {
if (!currentChannel) {
logger?.warning(`${LOG_PRESET} currentChannel doesn't exist`, currentChannel);
return;
}
if (isReacted) {
currentGroupChannel
?.deleteReaction(message, key)
currentChannel
.deleteReaction(message, key)
.then((res) => {
logger.info('Delete reaction success', res);
logger?.info(`${LOG_PRESET} Delete reaction success`, res);
})
.catch((err) => {
logger.warning('Delete reaction failed', err);
logger?.warning(`${LOG_PRESET} Delete reaction failed`, err);
});
} else {
currentGroupChannel
?.addReaction(message, key)
currentChannel
.addReaction(message, key)
.then((res) => {
logger.info('Add reaction success', res);
logger?.info(`${LOG_PRESET} Add reaction success`, res);
})
.catch((err) => {
logger.warning('Add reaction failed', err);
logger?.warning(`${LOG_PRESET} Add reaction failed`, err);
});
}
},
[currentGroupChannel],
[currentChannel],
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { GroupChannelUIView } from '../components/GroupChannelUI/GroupChannelUIView';
import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext';

jest.mock('../../../hooks/useSendbirdStateContext');

const mockUseSendbirdStateContext = useSendbirdStateContext as jest.Mock;

describe('GroupChannelUIView Integration Tests', () => {
const defaultProps = {
channelUrl: 'test-channel',
isInvalid: false,
renderChannelHeader: jest.fn(() => <div>Channel Header</div>),
renderMessageList: jest.fn(() => <div>Message List</div>),
renderMessageInput: jest.fn(() => <div>Message Input</div>),
};

beforeEach(() => {
mockUseSendbirdStateContext.mockImplementation(() => ({
stores: {
sdkStore: { error: null },
},
config: {
logger: { info: jest.fn() },
isOnline: true,
groupChannel: {
enableTypingIndicator: true,
typingIndicatorTypes: new Set(['text']),
},
},
}));
});

it('renders basic channel components correctly', () => {
render(<GroupChannelUIView {...defaultProps} />);

expect(screen.getByText('Channel Header')).toBeInTheDocument();
expect(screen.getByText('Message List')).toBeInTheDocument();
expect(screen.getByText('Message Input')).toBeInTheDocument();
});

it('renders loading placeholder when isLoading is true', () => {
render(<GroupChannelUIView {...defaultProps} isLoading={true} />);
// Placeholder is a just loading spinner in this case
expect(screen.getByRole('button')).toHaveClass('sendbird-icon-spinner');
});

it('renders invalid placeholder when channelUrl is missing', () => {
render(<GroupChannelUIView {...defaultProps} channelUrl="" />);
expect(screen.getByText('No channels')).toBeInTheDocument();
});

it('renders error placeholder when isInvalid is true', () => {
render(<GroupChannelUIView {...defaultProps} isInvalid={true} />);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});

it('renders SDK error placeholder when SDK has error', () => {
mockUseSendbirdStateContext.mockImplementation(() => ({
stores: {
sdkStore: { error: new Error('SDK Error') },
},
config: {
logger: { info: jest.fn() },
isOnline: true,
},
}));

render(<GroupChannelUIView {...defaultProps} />);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
expect(screen.getByText('Retry')).toBeInTheDocument();
});

it('renders custom placeholders when provided', () => {
const renderPlaceholderLoader = () => <div>Custom Loader</div>;
const renderPlaceholderInvalid = () => <div>Custom Invalid</div>;

const { rerender } = render(
<GroupChannelUIView
{...defaultProps}
isLoading={true}
renderPlaceholderLoader={renderPlaceholderLoader}
/>,
);
expect(screen.getByText('Custom Loader')).toBeInTheDocument();

rerender(
<GroupChannelUIView
{...defaultProps}
isInvalid={true}
renderPlaceholderInvalid={renderPlaceholderInvalid}
/>,
);
expect(screen.getByText('Custom Invalid')).toBeInTheDocument();
});
});
7 changes: 5 additions & 2 deletions src/modules/GroupChannel/components/FileViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import type { FileMessage } from '@sendbird/chat/message';

import { useGroupChannel } from '../../context/hooks/useGroupChannel';
import { FileViewerView } from './FileViewerView';
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';

export interface FileViewerProps {
Expand All @@ -11,7 +11,10 @@ export interface FileViewerProps {
}

export const FileViewer = (props: FileViewerProps) => {
const { deleteMessage, onBeforeDownloadFileMessage } = useGroupChannelContext();
const {
state: { onBeforeDownloadFileMessage },
actions: { deleteMessage },
} = useGroupChannel();
const { config } = useSendbirdStateContext();
const { logger } = config;
return (
Expand Down
3 changes: 2 additions & 1 deletion src/modules/GroupChannel/components/GroupChannelUI/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import GroupChannelHeader, { GroupChannelHeaderProps } from '../GroupChannelHead
import MessageList, { GroupChannelMessageListProps } from '../MessageList';
import MessageInputWrapper from '../MessageInputWrapper';
import { deleteNullish } from '../../../../utils/utils';
import { useGroupChannel } from '../../context/hooks/useGroupChannel';

export interface GroupChannelUIProps extends GroupChannelUIBasicProps {}

export const GroupChannelUI = (props: GroupChannelUIProps) => {
const context = useGroupChannelContext();
const { channelUrl, fetchChannelError } = context;
const { state: { channelUrl, fetchChannelError } } = useGroupChannel();

// Inject components to presentation layer
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import MessageContent, { MessageContentProps } from '../../../../ui/MessageConte

import SuggestedReplies, { SuggestedRepliesProps } from '../SuggestedReplies';
import SuggestedMentionListView from '../SuggestedMentionList/SuggestedMentionListView';
import type { OnBeforeDownloadFileMessageType } from '../../context/GroupChannelProvider';
import type { OnBeforeDownloadFileMessageType } from '../../context/types';
import { classnames, deleteNullish } from '../../../../utils/utils';

export interface MessageProps {
Expand Down
52 changes: 28 additions & 24 deletions src/modules/GroupChannel/components/Message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,42 @@ import { useIIFE } from '@sendbird/uikit-tools';
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
import { getSuggestedReplies, isSendableMessage } from '../../../../utils';
import { isDisabledBecauseFrozen, isDisabledBecauseMuted } from '../../context/utils';
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
import MessageView, { MessageProps } from './MessageView';
import FileViewer from '../FileViewer';
import RemoveMessageModal from '../RemoveMessageModal';
import { ThreadReplySelectType } from '../../context/const';
import { useGroupChannel } from '../../context/hooks/useGroupChannel';

export const Message = (props: MessageProps): React.ReactElement => {
const { config, emojiManager } = useSendbirdStateContext();
const {
loading,
currentChannel,
animatedMessageId,
setAnimatedMessageId,
scrollToMessage,
replyType,
threadReplySelectType,
isReactionEnabled,
toggleReaction,
nicknamesMap,
setQuoteMessage,
renderUserMentionItem,
filterEmojiCategoryIds,
onQuoteMessageClick,
onReplyInThreadClick,
onMessageAnimated,
onBeforeDownloadFileMessage,
messages,
updateUserMessage,
sendUserMessage,
resendMessage,
deleteMessage,
} = useGroupChannelContext();
state: {
loading,
currentChannel,
animatedMessageId,
replyType,
threadReplySelectType,
isReactionEnabled,
nicknamesMap,
renderUserMentionItem,
filterEmojiCategoryIds,
onQuoteMessageClick,
onReplyInThreadClick,
onMessageAnimated,
onBeforeDownloadFileMessage,
messages,
},
actions: {
toggleReaction,
setQuoteMessage,
setAnimatedMessageId,
scrollToMessage,
updateUserMessage,
sendUserMessage,
resendMessage,
deleteMessage,
},
} = useGroupChannel();

const { message } = props;
const initialized = !loading && Boolean(currentChannel);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import MessageInputWrapperView from './MessageInputWrapperView';
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
import { GroupChannelUIBasicProps } from '../GroupChannelUI/GroupChannelUIView';
import { useGroupChannel } from '../../context/hooks/useGroupChannel';

export interface MessageInputWrapperProps {
value?: string;
Expand All @@ -13,8 +13,8 @@ export interface MessageInputWrapperProps {
}

export const MessageInputWrapper = (props: MessageInputWrapperProps) => {
const context = useGroupChannelContext();
return <MessageInputWrapperView {...props} {...context} />;
const { state, actions } = useGroupChannel();
return <MessageInputWrapperView {...props} {...state} { ...actions} />;
};

export { VoiceMessageInputWrapper, type VoiceMessageInputWrapperProps } from './VoiceMessageInputWrapper';
Expand Down
44 changes: 24 additions & 20 deletions src/modules/GroupChannel/components/MessageList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import FrozenNotification from '../FrozenNotification';
import { SCROLL_BUFFER } from '../../../../utils/consts';
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
import TypingIndicatorBubble from '../../../../ui/TypingIndicatorBubble';
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
import { GroupChannelUIBasicProps } from '../GroupChannelUI/GroupChannelUIView';
import { deleteNullish } from '../../../../utils/utils';
import { getMessagePartsInfo } from './getMessagePartsInfo';
import { MessageProvider } from '../../../Message/context/MessageProvider';
import { getComponentKeyFromMessage } from '../../context/utils';
import { InfiniteList } from './InfiniteList';
import { useGroupChannel } from '../../context/hooks/useGroupChannel';

export interface GroupChannelMessageListProps {
className?: string;
Expand Down Expand Up @@ -67,25 +67,29 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
} = deleteNullish(props);

const {
channelUrl,
hasNext,
loading,
messages,
newMessages,
scrollToBottom,
isScrollBottomReached,
isMessageGroupingEnabled,
scrollRef,
scrollDistanceFromBottomRef,
scrollPositionRef,
currentChannel,
replyType,
scrollPubSub,
loadNext,
loadPrevious,
setIsScrollBottomReached,
resetNewMessages,
} = useGroupChannelContext();
state: {
channelUrl,
hasNext,
loading,
messages,
newMessages,
isScrollBottomReached,
isMessageGroupingEnabled,
currentChannel,
replyType,
scrollPubSub,
loadNext,
loadPrevious,
resetNewMessages,
scrollRef,
scrollPositionRef,
scrollDistanceFromBottomRef,
},
actions: {
scrollToBottom,
setIsScrollBottomReached,
},
} = useGroupChannel();

const store = useSendbirdStateContext();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
import RemoveMessageModalView, { RemoveMessageModalProps } from './RemoveMessageModalView';
import { useGroupChannel } from '../../context/hooks/useGroupChannel';

export const RemoveMessageModal = (props: RemoveMessageModalProps) => {
const { deleteMessage } = useGroupChannelContext();
const { actions: { deleteMessage } } = useGroupChannel();
return <RemoveMessageModalView {...props} deleteMessage={deleteMessage} />;
};

Expand Down
Loading