@microsoft/mgt-chat
Version:
Chat component for providing Teams conversations via a React component built using the ACS UI library
175 lines (154 loc) • 5.47 kB
text/typescript
import { MessageThreadProps, ChatMessage as ACSChatMessage } from '@azure/communication-react';
import { Chat } from '@microsoft/microsoft-graph-types';
import { ActiveAccountChanged, IGraph, LoginChangedEvent, Providers, ProviderState } from '@microsoft/mgt-element';
import { produce } from 'immer';
import { loadChat, loadChatThread, loadMoreChatMessages, MessageCollection } from './graph.chat';
import { graphChatMessageToACSChatMessage } from './acs.chat';
type GraphChatClient = Pick<
MessageThreadProps,
| 'userId'
| 'messages'
| 'participantCount'
| 'disableEditing'
| 'onLoadPreviousChatMessages'
| 'numberOfChatMessagesToReload'
>;
type StatefulClient<T> = {
/**
* Get the current state of the client
*/
getState(): T;
/**
* Register a callback to receive state updates
*
* @param handler Callback to receive state updates
*/
onStateChange(handler: (state: T) => void): void;
/**
* Remove a callback from receiving state updates
*
* @param handler Callback to be unregistered
*/
offStateChange(handler: (state: T) => void): void;
};
const MessageCreatedComparator = (a: ACSChatMessage, b: ACSChatMessage) =>
a.createdOn.getTime() - b.createdOn.getTime();
class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
private _subscribers: ((state: GraphChatClient) => void)[] = [];
private _messagesPerCall = 5;
private _nextLink: string = '';
private _chat?: Chat = undefined;
constructor() {
this.updateCurrentUserId();
Providers.globalProvider.onStateChanged(this.onStateChanged);
Providers.globalProvider.onActiveAccountChanged(this.onActiveAccountChanged);
}
public onStateChange(handler: (state: GraphChatClient) => void): void {
if (!this._subscribers.includes(handler)) {
this._subscribers.push(handler);
}
}
public offStateChange(handler: (state: GraphChatClient) => void): void {
const index = this._subscribers.indexOf(handler);
if (index !== -1) {
this._subscribers = this._subscribers.splice(index, 1);
}
}
private notifyStateChange() {
this._subscribers.forEach(handler => handler(this._state));
}
public getState(): GraphChatClient {
return this._state;
}
private onStateChanged = (e: LoginChangedEvent) => {
switch (Providers.globalProvider.state) {
case ProviderState.SignedIn:
this.updateCurrentUserId();
// update userId
// load messages
// configure subscriptions
// emit new state;
return;
case ProviderState.SignedOut:
// clear userId
// clear subscriptions
// clear messages
// emit new state
return;
case ProviderState.Loading:
default:
// do nothing for now
return;
}
};
private onActiveAccountChanged = (e: ActiveAccountChanged) => {
this.updateCurrentUserId();
};
private updateCurrentUserId() {
this.userId = Providers.globalProvider.getActiveAccount?.().id.split('.')[0] || '';
}
private _userId: string = '';
private set userId(userId: string) {
if (this._userId === userId) {
return;
}
this._userId = userId;
this._state = produce(this._state, (draft: GraphChatClient) => {
draft.userId = userId;
});
this.notifyStateChange();
}
private _chatId: string = '';
public set chatId(value: string) {
// take no action if the chatId is the same
if (this._chatId === value) {
return;
}
this._chatId = value;
void this.updateFollowedChat();
}
private async updateFollowedChat() {
// Subscribe to notifications for messages
// Subscribe to notifications for the chat itself?
// load Chat Data
this._chat = await loadChat(this.graph, this._chatId);
// Allow messages to be loaded via the loadMoreMessages callback
this._state = produce(this._state, (draft: GraphChatClient) => {
draft.participantCount = this._chat?.members?.length || 0;
});
this.notifyStateChange();
}
private loadMoreMessages = async () => {
let result: MessageCollection;
if (this._nextLink) {
result = await loadMoreChatMessages(this.graph, this._nextLink);
} else {
result = await loadChatThread(this.graph, this._chatId, this._messagesPerCall);
}
this._nextLink = result.nextLink;
this._state = produce(this._state, (draft: GraphChatClient) => {
const nextMessages = result.value
// trying to filter out system messages on the graph request causes a 400
// delted messages are returned as messages with no content, which we can't filter on the graph request
// so we filter them out here
.filter(m => m.messageType === 'message' && m.body?.content)
.map(m => graphChatMessageToACSChatMessage(m, this._userId));
draft.messages = nextMessages.concat(draft.messages as ACSChatMessage[]).sort(MessageCreatedComparator);
});
this.notifyStateChange();
// return true when there are no more messages to load
return !Boolean(this._nextLink);
};
private get graph(): IGraph {
return Providers.globalProvider.graph.forComponent('mgt-chat');
}
private _state: GraphChatClient = {
userId: '',
messages: [],
participantCount: 0,
disableEditing: true,
onLoadPreviousChatMessages: this.loadMoreMessages,
numberOfChatMessagesToReload: this._messagesPerCall
};
}
export { StatefulGraphChatClient };