Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle switching the active account #2613

Merged
merged 19 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
17 changes: 14 additions & 3 deletions packages/mgt-chat/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import { ErrorBar, FluentThemeProvider, MessageThread, SendBox } from '@azure/communication-react';
import { Person, PersonCardInteraction, Spinner } from '@microsoft/mgt-react';
import { FluentTheme } from '@fluentui/react';
import { FluentTheme, MessageBarType } from '@fluentui/react';
import { FluentProvider, makeStyles, shorthands, teamsLightTheme } from '@fluentui/react-components';
import { useGraphChatClient } from '../../statefulClient/useGraphChatClient';
import ChatHeader from '../ChatHeader/ChatHeader';
import ChatMessageBar from '../ChatMessageBar/ChatMessageBar';
import { registerAppIcons } from '../styles/registerIcons';
import { ManageChatMembers } from '../ManageChatMembers/ManageChatMembers';
import { StatefulGraphChatClient } from 'src/statefulClient/StatefulGraphChatClient';
Expand Down Expand Up @@ -114,8 +115,18 @@ export const Chat = ({ chatId }: IMgtChatProps) => {
{chatState.status}
</div>
)}
{chatState.status === 'initial' && <p>Select a chat to display messages.</p>}
{chatState.status === 'no messages' && <p>No messages to display.</p>}
{chatState.status === 'initial' && (
musale marked this conversation as resolved.
Show resolved Hide resolved
<ChatMessageBar messageBarType={MessageBarType.info} message={'Select a chat to display messages.'} />
musale marked this conversation as resolved.
Show resolved Hide resolved
)}
{chatState.status === 'no messages' && (
<ChatMessageBar
messageBarType={MessageBarType.error}
message={`No messages were found for the id ${chatId}.`}
/>
)}
{chatState.status === 'no chat id' && (
<ChatMessageBar messageBarType={MessageBarType.error} message={'A valid chat id is required.'} />
)}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { MessageBar, MessageBarType } from '@fluentui/react';

interface ChatMessageBarProps {
messageBarType: MessageBarType;
message: string;
}

const ChatMessageBar = ({ messageBarType, message }: ChatMessageBarProps) => {
return <MessageBar messageBarType={messageBarType}> {message}</MessageBar>;
};

export default ChatMessageBar;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { buttonIconStyles } from './common.styles';

const registerAppIcons = () => {
const icons = Object.assign(DEFAULT_COMPONENT_ICONS, {
// TODO: Register the info and errorbadge icons
'add-friend': (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
132 changes: 90 additions & 42 deletions packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
MembersAddedEventMessageDetail,
MembersDeletedEventMessageDetail
} from '@microsoft/microsoft-graph-types';
import { IGraph, LoginChangedEvent, Providers, ProviderState } from '@microsoft/mgt-element';
import { ActiveAccountChanged, IGraph, LoginChangedEvent, Providers, ProviderState } from '@microsoft/mgt-element';
import { produce } from 'immer';
import { v4 as uuid } from 'uuid';
import {
Expand All @@ -44,6 +44,7 @@ import { IDynamicPerson } from '@microsoft/mgt-react';
import { updateMessageContentWithImage } from './updateMessageContentWithImage';
import { graph } from '../utils/graph';
import { currentUserId, getCurrentUser, currentUserName } from '../utils/currentUser';
import { GraphError } from '@microsoft/microsoft-graph-client';

// 1x1 grey pixel
const placeholderImageContent =
Expand Down Expand Up @@ -94,6 +95,7 @@ type GraphChatClient = Pick<
| 'creating server connections'
| 'subscribing to notifications'
| 'loading messages'
| 'no chat id'
| 'no messages'
| 'ready'
| 'error';
Expand Down Expand Up @@ -241,14 +243,14 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
* @memberof StatefulGraphChatClient
*/
private readonly onLoginStateChanged = (e: LoginChangedEvent) => {
switch (Providers.globalProvider.state) {
switch (e.detail) {
case ProviderState.SignedIn:
// update userId and displayName
this.updateUserInfo();
// load messages?
// configure subscriptions
// emit new state;
if (this._chatId) {
if (this.chatId) {
void this.updateFollowedChat();
}
return;
Expand All @@ -265,13 +267,16 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
}
};

private readonly onActiveAccountChanged = () => {
this.unregisterNotifications();
this.clearCurrentUserMessages();
// void this.closeCurrentSignalRConnections();
// void this.reconnectSignalRConnection();
this.updateUserInfo();
void this.updateFollowedChat();
private readonly onActiveAccountChanged = (e: ActiveAccountChanged) => {
if (this.userId !== e.detail.id) {
musale marked this conversation as resolved.
Show resolved Hide resolved
this.unregisterNotifications();
this.clearCurrentUserMessages();
sessionStorage.removeItem('graph-subscriptions');
// void this.closeCurrentSignalRConnections();
// void this.reconnectSignalRConnection();
this.updateUserInfo();
void this.updateFollowedChat();
}
};

private unregisterNotifications() {
Expand Down Expand Up @@ -300,15 +305,35 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
this.updateCurrentUserName();
}

/**
* Changes the userDisplayName to the current value.
*/
private updateCurrentUserName() {
this._userDisplayName = currentUserName();
}

/**
* Changes the current user ID value to the current value.
*/
private updateCurrentUserId() {
this.userId = currentUserId();
}

/**
* Current User ID.
*/
private _userId = '';

/**
* Returns the current User ID.
*/
public get userId() {
return this._userId;
}

/**
* Sets the current User ID and updates the state value.
*/
private set userId(userId: string) {
if (this._userId === userId) {
return;
Expand All @@ -319,15 +344,30 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
});
}

/**
* Current chat ID.
*/
private _chatId = '';

/**
* Get the current chat ID.
*/
public get chatId() {
return this._chatId;
}

/**
* Set the current chat ID and tries to get the chat data.
*/
public set chatId(value: string) {
// take no action if the chatId is the same
if (this._chatId === value) {
if (value && this._chatId === value) {
return;
}
this._chatId = value;
void this.updateFollowedChat();
if (this._chatId) {
void this.updateFollowedChat();
}
}

/**
Expand All @@ -337,34 +377,44 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
* @memberof StatefulGraphChatClient
*/
private async updateFollowedChat() {
// reset state to initial
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'initial';
draft.messages = [];
draft.chat = undefined;
draft.participants = [];
});
// Subscribe to notifications for messages
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'creating server connections';
});
// avoid subscribing to a resource with an empty chatId
if (this._chatId) {
musale marked this conversation as resolved.
Show resolved Hide resolved
const promises: Promise<void>[] = [];
promises.push(this.loadChatData());
// subscribing to notifications will trigger the chatMessageNotificationsSubscribed event
// this client will then load the chat and messages when that event listener is called
promises.push(
this._notificationClient.subscribeToChatNotifications(this._userId, this._chatId, this._eventEmitter, () =>
// reset state to initial
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'initial';
draft.messages = [];
draft.chat = undefined;
draft.participants = [];
});
// Subscribe to notifications for messages
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'creating server connections';
});

try {
// Prefer sequential promise resolving to catch loading message errors
// TODO: in parallel promise resolving, find out how to trigger different
// TODO: state for failed subscriptions in GraphChatClient.onSubscribeFailed
await this.loadChatData();
await this._notificationClient.subscribeToChatNotifications(
this._userId,
this._chatId,
this._eventEmitter,
() =>
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'subscribing to notifications';
})
);
} catch (error) {
if (error instanceof GraphError) {
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'subscribing to notifications';
})
)
);
await Promise.all(promises);
draft.status = 'no messages';
});
}
}
} else {
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'no messages';
draft.status = 'no chat id';
});
}
}
Expand All @@ -373,15 +423,13 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'loading messages';
});
if (this._chatId) {
try {
this._chat = await loadChat(this.graph, this._chatId);
const messages: MessageCollection = await loadChatThread(this.graph, this._chatId, this._messagesPerCall);
await this.writeMessagesToState(messages);
} else {
this.notifyStateChange((draft: GraphChatClient) => {
draft.status = 'no messages';
});
} catch (error) {
gavinbarron marked this conversation as resolved.
Show resolved Hide resolved
return Promise.reject(error);
}
const messages: MessageCollection = await loadChatThread(this.graph, this._chatId, this._messagesPerCall);
await this.writeMessagesToState(messages);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/mgt-chat/src/statefulClient/useGraphChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { StatefulGraphChatClient } from './StatefulGraphChatClient';

export const useGraphChatClient = (chatId: string): StatefulGraphChatClient => {
const [chatClient] = useState<StatefulGraphChatClient>(new StatefulGraphChatClient());
chatClient.chatId = chatId;
if (chatClient.chatId !== chatId) {
musale marked this conversation as resolved.
Show resolved Hide resolved
chatClient.chatId = chatId;
}

return chatClient;
};
16 changes: 10 additions & 6 deletions packages/mgt-element/src/providers/IProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export abstract class IProvider implements AuthenticationProvider {
public setState(state: ProviderState) {
if (state !== this._state) {
this._state = state;
this._loginChangedDispatcher.fire({});
this._loginChangedDispatcher.fire({ detail: this._state });
}
}

Expand Down Expand Up @@ -211,7 +211,7 @@ export abstract class IProvider implements AuthenticationProvider {
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public setActiveAccount?(user: IProviderAccount) {
this.fireActiveAccountChanged();
this.fireActiveAccountChanged({ detail: user });
}

/**
Expand Down Expand Up @@ -239,8 +239,8 @@ export abstract class IProvider implements AuthenticationProvider {
*
* @memberof IProvider
*/
private fireActiveAccountChanged() {
this._activeAccountChangedDispatcher.fire({});
private fireActiveAccountChanged(account: { detail: IProviderAccount }) {
this._activeAccountChangedDispatcher.fire(account);
}

/**
Expand Down Expand Up @@ -272,15 +272,19 @@ export abstract class IProvider implements AuthenticationProvider {
* @interface ActiveAccountChanged
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ActiveAccountChanged {}
export interface ActiveAccountChanged {
detail: IProviderAccount;
}
/**
* loginChangedEvent
*
* @export
* @interface LoginChangedEvent
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface LoginChangedEvent {}
export interface LoginChangedEvent {
detail: ProviderState;
}

/**
* LoginType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const ChatListTemplate = (props: MgtTemplateProps & ChatInteractionProps) => {
const { value } = props.dataContext;
const chats: Chat[] = value;
// Select a default chat to display
props.onSelected(chats[0]);
// props.onSelected(chats[0]);
return (
<ul>
{chats.map(c => (
Expand Down
Loading
Loading