Skip to content

Commit

Permalink
Add chat thread story (#90)
Browse files Browse the repository at this point in the history
* add story for chat Thread

* Add changelog for SDK

* rename variables and remove comments

* fix lint

* resolve comments and rename variables

* update tests

* add fake data for test mode

* test still doesnt work, need to disable it for now

* remove unused variable

* update tests

Co-authored-by: Eason Yang <[email protected]>
  • Loading branch information
ytyeason and Eason Yang authored Mar 25, 2021
1 parent 525e87c commit 77b25ac
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Fix small bugs for chat messages mapper",
"packageName": "@azure/communication-ui",
"email": "[email protected]",
"dependentChangeType": "patch"
}
10 changes: 7 additions & 3 deletions packages/communication-ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';
import { withKnobs } from '@storybook/addon-knobs';
import { FluentThemeProvider } from '../src/providers/FluentThemeProvider';
import { LIGHT, DARK, THEMES } from '../src/constants/themes';
import { initializeIcons, loadTheme } from '@fluentui/react';
import { initializeIcons, loadTheme, mergeStyles } from '@fluentui/react';
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { BackToTop, TableOfContents } from 'storybook-docs-toc';

Expand All @@ -17,9 +17,13 @@ export const parameters = {
docs: {
container: props => (
<React.Fragment>
<TableOfContents />
<div className={mergeStyles({'& nav': { right: 0 }})}>
<TableOfContents />
</div>
<DocsContainer {...props} />
<BackToTop />
<div className={mergeStyles({'> button': { right: 0 }})}>
<BackToTop />
</div>
</React.Fragment>
),
},
Expand Down
25 changes: 16 additions & 9 deletions packages/communication-ui/review/communication-ui.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export type ChatMessage = {
senderId?: string;
senderDisplayName?: string;
statusToRender?: MessageStatus;
attached?: MessageAttachedStatus;
attached?: MessageAttachedStatus | boolean;
mine?: boolean;
clientMessageId?: string;
};
Expand All @@ -196,7 +196,7 @@ export interface ChatMessageWithClientMessageId extends ChatMessage_2 {
export const ChatProvider: (props: ChatProviderProps & ErrorHandlingProps) => JSX.Element;

// @public (undocumented)
export const ChatThread: (props: Pick<ChatThreadProps & ErrorHandlingProps & ChatMessagePropsFromContext, "onErrorCallback" | "styles" | "disableNewMessageButton" | "onRenderReadReceipt" | "onRenderAvatar" | "onRenderLoadPreviousMessagesButton" | "onRenderNewMessageButton">) => React_2.ReactElement<any, string | ((props: any) => React_2.ReactElement<any, any> | null) | (new (props: any) => React_2.Component<any, any, any>)>;
export const ChatThread: (props: Pick<ChatThreadProps & ErrorHandlingProps & ChatMessagePropsFromContext, "onErrorCallback" | "styles" | "disableJumpToNewMessageButton" | "onRenderReadReceipt" | "onRenderAvatar" | "onRenderLoadPreviousMessagesButton" | "onRenderJumpToNewMessageButton">) => React_2.ReactElement<any, string | ((props: any) => React_2.ReactElement<any, any> | null) | (new (props: any) => React_2.Component<any, any, any>)>;

// @public (undocumented)
export const ChatThreadComponent: (props: ChatThreadProps & ErrorHandlingProps & ChatMessagePropsFromContext) => JSX.Element;
Expand All @@ -218,13 +218,13 @@ export type ChatThreadProps = {
userId: string;
chatMessages: ChatMessage[];
styles?: ChatThreadStylesProps;
disableNewMessageButton?: boolean;
disableJumpToNewMessageButton?: boolean;
disableLoadPreviousMessage?: boolean;
disableReadReceipt?: boolean;
onSendReadReceipt?: () => Promise<void>;
onRenderReadReceipt?: (readReceiptProps: ReadReceiptProps) => JSX.Element;
onRenderAvatar?: (userId: string) => JSX.Element;
onRenderNewMessageButton?: (newMessageButtonProps: NewMessageButtonProps) => JSX.Element;
onRenderJumpToNewMessageButton?: (newMessageButtonProps: JumpToNewMessageButtonProps) => JSX.Element;
onLoadPreviousMessages?: () => void;
onRenderLoadPreviousMessagesButton?: (loadPreviousMessagesButton: LoadPreviousMessagesButtonProps) => JSX.Element;
};
Expand Down Expand Up @@ -636,6 +636,12 @@ export const isMobileSession: () => boolean;
// @public (undocumented)
export function isSelectedDeviceInList<T extends AudioDeviceInfo | VideoDeviceInfo>(device: T, list: T[]): boolean;

// @public (undocumented)
export interface JumpToNewMessageButtonProps {
// (undocumented)
onClick: () => void;
}

// @public
export const LIGHT = "light";

Expand All @@ -653,6 +659,12 @@ export type ListParticipant = {
onMute?: () => void;
};

// @public (undocumented)
export interface LoadPreviousMessagesButtonProps {
// (undocumented)
onClick: () => void;
}

// @public (undocumented)
export type LocalDeviceSettingsContainerProps = {
videoDeviceList: VideoDeviceInfo[];
Expand Down Expand Up @@ -1252,11 +1264,6 @@ export interface VideoTileStylesProps {
export const WithErrorHandling: (Component: (props: any & ErrorHandlingProps) => JSX.Element, props: any & ErrorHandlingProps) => JSX.Element;


// Warnings were encountered during analysis:
//
// src/components/ChatThread.tsx:183:3 - (ae-forgotten-export) The symbol "NewMessageButtonProps" needs to be exported by the entry point index.d.ts
// src/components/ChatThread.tsx:185:3 - (ae-forgotten-export) The symbol "LoadPreviousMessagesButtonProps" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

```
87 changes: 65 additions & 22 deletions packages/communication-ui/src/components/ChatThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { LiveAnnouncer, LiveMessage } from 'react-aria-live';
import { ErrorHandlingProps } from '../providers';
import { formatTimestampForChatMessage, propagateError, WithErrorHandling } from '../utils';
import { CLICK_TO_LOAD_MORE_MESSAGES, NEW_MESSAGES } from '../constants';
import { ChatMessage as WebUiChatMessage, MessageStatus } from '../types';
import { ChatMessage as WebUiChatMessage } from '../types';
import { ReadReceiptComponent, ReadReceiptProps } from './ReadReceipt';
import { connectFuncsToContext, ChatMessagePropsFromContext, MapToChatMessageProps } from '../consumers';

Expand Down Expand Up @@ -116,9 +116,7 @@ const didUserSendTheLatestMessage = (
} else {
return (
!isMessageSame(latestMessageFromNewMessages, latestMessageFromPreviousMessages) &&
latestMessageFromNewMessages.senderId === userId &&
latestMessageFromNewMessages.statusToRender !== MessageStatus.SEEN &&
latestMessageFromNewMessages.statusToRender !== MessageStatus.FAILED
latestMessageFromNewMessages.senderId === userId
);
}
}
Expand All @@ -139,11 +137,11 @@ export interface ChatThreadStylesProps {
readReceiptContainer?: (mine: boolean) => IStyle;
}

export interface NewMessageButtonProps {
export interface JumpToNewMessageButtonProps {
onClick: () => void;
}

const DefaultNewMessageButton = (props: NewMessageButtonProps): JSX.Element => {
const DefaultJumpToNewMessageButton = (props: JumpToNewMessageButtonProps): JSX.Element => {
const { onClick } = props;
return (
<PrimaryButton className={newMessageButtonStyle} onClick={onClick}>
Expand Down Expand Up @@ -171,28 +169,73 @@ const DefaultLoadPreviousMessagesButton = (props: LoadPreviousMessagesButtonProp
};

export type ChatThreadProps = {
/**
* The userId of the current user.
*/
userId: string;
/**
* The chat messages to render in chat thread. Chat messages need to have type `WebUiChatMessage`
*/
chatMessages: WebUiChatMessage[];
/**
* Custom CSS Styling.
*/
styles?: ChatThreadStylesProps;
disableNewMessageButton?: boolean;
/**
* Whether the new message button is disabled.
* @defaultValue `false`
*/
disableJumpToNewMessageButton?: boolean;
/**
* Whether the load previous message button is disabled.
* @defaultValue `true`
*/
disableLoadPreviousMessage?: boolean;
/**
* Whether the read receipt for each message is disabled.
* @defaultValue `true`
*/
disableReadReceipt?: boolean;
/**
* onSendReadReceipt event handler. `() => Promise<void>`
*/
onSendReadReceipt?: () => Promise<void>;
/**
* onRenderReadReceipt event handler. `(readReceiptProps: ReadReceiptProps) => JSX.Element`
*/
onRenderReadReceipt?: (readReceiptProps: ReadReceiptProps) => JSX.Element;
/**
* onRenderAvatar event handler. `(userId: string) => JSX.Element`
*/
onRenderAvatar?: (userId: string) => JSX.Element;
onRenderNewMessageButton?: (newMessageButtonProps: NewMessageButtonProps) => JSX.Element;
/**
* onRenderJumpToNewMessageButton event handler. `(newMessageButtonProps: JumpToNewMessageButtonProps) => JSX.Element`
*/
onRenderJumpToNewMessageButton?: (newMessageButtonProps: JumpToNewMessageButtonProps) => JSX.Element;
/**
* onLoadPreviousMessages event handler.
*/
onLoadPreviousMessages?: () => void;
/**
* onRenderLoadPreviousMessagesButton event handler. `(loadPreviousMessagesButton: LoadPreviousMessagesButtonProps) => JSX.Element`
*/
onRenderLoadPreviousMessagesButton?: (loadPreviousMessagesButton: LoadPreviousMessagesButtonProps) => JSX.Element;
};

// A Chatthread will be fed many messages so it will try to map out the messages out of the props and feed them into a
// Chat item. We need to be smarter and figure out for the last N messages are they all of the same person or not?
/**
* `ChatThread` allows you to easily create a component for rendering chat messages, handling scrolling behavior of new/old messages and customizing icons & controls inside the chat thread.
*
* Users will need to provide at least chat messages and userId to render the `ChatThread` component.
* Users can also customize `ChatThread` by passing in their own Avatar, `ReadReceipt` icon, `JumpToNewMessageButton`, `LoadPreviousMessagesButton` and the behavior of these controls.
*
* `ChatThread` internally uses the `Chat` & `Chat.Message` component from `@fluentui/react-northstar`. You can checkout the details about these [two components](https://fluentsite.z22.web.core.windows.net/0.53.0/components/chat/props).
*/
export const ChatThreadComponentBase = (props: ChatThreadProps & ErrorHandlingProps): JSX.Element => {
const {
chatMessages: newChatMessages,
userId,
styles,
disableNewMessageButton = false,
disableJumpToNewMessageButton = false,
disableReadReceipt = true,
disableLoadPreviousMessage = true,
onSendReadReceipt,
Expand All @@ -201,7 +244,7 @@ export const ChatThreadComponentBase = (props: ChatThreadProps & ErrorHandlingPr
onErrorCallback,
onLoadPreviousMessages,
onRenderLoadPreviousMessagesButton,
onRenderNewMessageButton
onRenderJumpToNewMessageButton
} = props;

const [chatMessages, setChatMessages] = useState<WebUiChatMessage[]>([]);
Expand Down Expand Up @@ -289,14 +332,14 @@ export const ChatThreadComponentBase = (props: ChatThreadProps & ErrorHandlingPr
* unmounts.
*/
useEffect(() => {
window.addEventListener('click', sendReadReceiptIfAtBottom);
window.addEventListener('focus', sendReadReceiptIfAtBottom);
chatScrollDivRef.current.addEventListener('scroll', handleScroll);
window && window.addEventListener('click', sendReadReceiptIfAtBottom);
window && window.addEventListener('focus', sendReadReceiptIfAtBottom);
chatScrollDivRef.current && chatScrollDivRef.current.addEventListener('scroll', handleScroll);
const chatScrollDiv = chatScrollDivRef.current;
return () => {
window.removeEventListener('click', sendReadReceiptIfAtBottom);
window.removeEventListener('focus', sendReadReceiptIfAtBottom);
chatScrollDiv.removeEventListener('scroll', handleScroll);
window && window.removeEventListener('click', sendReadReceiptIfAtBottom);
window && window.removeEventListener('focus', sendReadReceiptIfAtBottom);
chatScrollDiv && chatScrollDiv.removeEventListener('scroll', handleScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -434,12 +477,12 @@ export const ChatThreadComponentBase = (props: ChatThreadProps & ErrorHandlingPr
<Chat styles={styles?.chatContainer ?? chatStyle} items={messagesToDisplay} />
</LiveAnnouncer>
</Ref>
{existsNewMessage && !disableNewMessageButton && (
{existsNewMessage && !disableJumpToNewMessageButton && (
<div className={mergeStyles(newMessageButtonContainerStyle, styles?.newMessageButtonContainer)}>
{onRenderNewMessageButton ? (
onRenderNewMessageButton({ onClick: scrollToBottom })
{onRenderJumpToNewMessageButton ? (
onRenderJumpToNewMessageButton({ onClick: scrollToBottom })
) : (
<DefaultNewMessageButton onClick={scrollToBottom} />
<DefaultJumpToNewMessageButton onClick={scrollToBottom} />
)}
</div>
)}
Expand Down
7 changes: 6 additions & 1 deletion packages/communication-ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export { SendBoxComponent } from './SendBox';
export { ReadReceiptComponent } from './ReadReceipt';
export type { ReadReceiptProps } from './ReadReceipt';
export { ChatThreadComponent, ChatThread } from './ChatThread';
export type { ChatThreadProps, ChatThreadStylesProps } from './ChatThread';
export type {
ChatThreadProps,
ChatThreadStylesProps,
JumpToNewMessageButtonProps,
LoadPreviousMessagesButtonProps
} from './ChatThread';
export { StreamMedia } from './StreamMedia';
export { ParticipantItem } from './ParticipantItem';
export type { ParticipantItemProps } from './ParticipantItem';
Expand Down
21 changes: 18 additions & 3 deletions packages/communication-ui/src/consumers/MapToChatMessageProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export const updateMessagesWithAttached = (
}

const messageWithAttached = { ...message, attached, mine, statusToRender };
// Remove the clientMessageId field as it's only needed to getMessageStatus, not needed by ChatThread component
// When we migrate to declarative, ideally we should remove the clientMessageId from the WebUiChatMessage type.
delete messageWithAttached.clientMessageId;
newChatMessages.push(messageWithAttached);
return message;
});
Expand Down Expand Up @@ -174,6 +177,8 @@ const convertSdkChatMessagesToWebUiChatMessages = (
createdOn: chatMessage.createdOn,
senderId: chatMessage.sender?.communicationUserId,
senderDisplayName: chatMessage.senderDisplayName,
// clientMessageId field is attached by useSendMessage hooks,
// and it's needed to filter out failed messages, will not used by ChatThread component.
clientMessageId: chatMessage.clientMessageId
};
}) ?? [];
Expand Down Expand Up @@ -203,18 +208,28 @@ export const MapToChatMessageProps = (): ChatMessagePropsFromContext => {
return isLargeParticipantsGroup(threadMembers);
}, [threadMembers]);
const sendReadReceipt = useSendReadReceipt();
const [messagesNumber, setMessagesNumber] = useState<number>(20);
const [messagesNumber, setMessagesNumber] = useState<number>(25);
const [disableLoadPreviousMessage, setDisableLoadPreviousMessage] = useState<boolean>(false);
const chatMessages = useMemo(() => {
sdkChatMessages && messagesNumber >= sdkChatMessages.length && setDisableLoadPreviousMessage(true);
sdkChatMessages && messagesNumber >= sdkChatMessages.length
? !disableLoadPreviousMessage && setDisableLoadPreviousMessage(true)
: disableLoadPreviousMessage && setDisableLoadPreviousMessage(false);
return convertSdkChatMessagesToWebUiChatMessages(
sdkChatMessages?.slice(Math.max(sdkChatMessages.length - messagesNumber, 0)) ?? [],
failedMessageIds,
isLargeGroup,
userId,
isMessageSeen
);
}, [failedMessageIds, isLargeGroup, isMessageSeen, sdkChatMessages, userId, messagesNumber]);
}, [
failedMessageIds,
isLargeGroup,
isMessageSeen,
sdkChatMessages,
userId,
messagesNumber,
disableLoadPreviousMessage
]);

const onSendReadReceipt = useCallback(async () => {
const messageId = getLatestIncomingMessageId(chatMessages, userId);
Expand Down
Loading

0 comments on commit 77b25ac

Please sign in to comment.