Skip to content

Commit

Permalink
Merge branch 'next/mgt-chat' into fix-account-switch
Browse files Browse the repository at this point in the history
  • Loading branch information
musale committed Jul 25, 2023
2 parents d91ce07 + 46b0d0f commit 3a8e86a
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 39 deletions.
6 changes: 4 additions & 2 deletions samples/react-contoso/.env.sample
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
BROWSER="none"
SKIP_PREFLIGHT_CHECK=true
FAST_REFRESH = false
FAST_REFRESH=false
REACT_APP_SITE_NAME="Contoso Dashboard"
REACT_APP_CLIENT_ID="ed072e38-e76e-45ae-ab76-073cb95495bb"
REACT_APP_CLIENT_ID="00000000-0000-0000-0000-000000000000"
REACT_APP_URL_AZURE_FUNCTION="https://contoso.azurewebsites.net"
REACT_APP_BACKEND_CLIENT_ID="00000000-0000-0000-0000-000000000000"

4 changes: 2 additions & 2 deletions samples/react-contoso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo
In the root directory for the repository:

```
yarn && yarn build && yarn watch:react`
yarn && yarn build && yarn watch:react
```

This will:
Expand All @@ -18,4 +18,4 @@ This will:
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.
You will also see any lint errors in the console.
You will also see any lint errors in the console.
10 changes: 10 additions & 0 deletions samples/react-contoso/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { App } from './App';
import { mergeStyles } from '@fluentui/react';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { Providers, LoginType } from '@microsoft/mgt-element';
import { brokerSettings } from '@microsoft/mgt-chat';

// Inject some global styles
mergeStyles({
Expand All @@ -14,13 +15,22 @@ mergeStyles({
}
});

brokerSettings.functionHost = process.env.REACT_APP_URL_AZURE_FUNCTION!;
brokerSettings.appId = process.env.REACT_APP_BACKEND_CLIENT_ID!;

Providers.globalProvider = new Msal2Provider({
clientId: process.env.REACT_APP_CLIENT_ID!,
loginType: LoginType.Redirect,
redirectUri: window.location.protocol + '//' + window.location.host,
scopes: [
'Bookmark.Read.All',
'Calendars.Read',
'Chat.Create',
'Chat.Read',
'Chat.ReadBasic',
'Chat.ReadWrite',
'ChatMember.ReadWrite',
'ChatMessage.Send',
'ExternalItem.Read.All',
'Files.Read',
'Files.Read.All',
Expand Down
137 changes: 137 additions & 0 deletions samples/react-contoso/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as React from 'react';
import { PageHeader } from '../components/PageHeader';
import { Get } from '@microsoft/mgt-react';
import { Loading } from '../components/Loading';
import {
shorthands,
makeStyles,
mergeClasses,
Button,
Dialog,
DialogTrigger,
DialogSurface,
DialogBody,
DialogTitle
} from '@fluentui/react-components';
import { Chat as GraphChat } from '@microsoft/microsoft-graph-types';
import { Chat, NewChat } from '@microsoft/mgt-chat';
import ChatListTemplate from './Chats/ChatListTemplate';

const useStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'row'
},
panels: {
...shorthands.padding('10px')
},
main: {
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap',
width: '300px',
minWidth: '300px',
...shorthands.overflow('auto'),
maxHeight: '80vh',
borderRightColor: 'var(--neutral-stroke-rest)',
borderRightStyle: 'solid',
borderRightWidth: '1px'
},
side: {
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap',
width: '80%',
...shorthands.overflow('auto'),
maxHeight: '80vh',
height: '100%'
},
newChat: {
paddingBottom: '10px',
marginRight: '0px',
marginLeft: 'auto'
},
dialog: {
display: 'block'
}
});

export const ChatPage: React.FunctionComponent = () => {
const styles = useStyles();
const [selectedChat, setSelectedChat] = React.useState<GraphChat>();
const [isNewChatOpen, setIsNewChatOpen] = React.useState(false);

const chatSelected = (e: GraphChat) => {
if (e.id !== selectedChat?.id && isNewChatOpen) {
setIsNewChatOpen(false);
}
setSelectedChat(e);
};

return (
<>
<PageHeader
title={'Chats'}
description={'Stay in touch with your teammates and navigate your chats'}
></PageHeader>

<div className={styles.container}>
<div className={mergeClasses(styles.panels, styles.main)}>
<div className={styles.newChat}>
<Dialog open={isNewChatOpen}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary" onClick={() => setIsNewChatOpen(true)}>
New Chat
</Button>
</DialogTrigger>
<DialogSurface>
<DialogBody className={styles.dialog}>
<DialogTitle>New Chat</DialogTitle>
<NewChat
onChatCreated={chatSelected}
onCancelClicked={() => {
setIsNewChatOpen(false);
}}
hideTitle={true}
></NewChat>
</DialogBody>
</DialogSurface>
</Dialog>
</div>
<ChatList chatSelected={selectedChat} onChatSelected={setSelectedChat}></ChatList>
</div>
<div className={styles.side}>{selectedChat && <Chat chatId={selectedChat.id!}></Chat>}</div>
</div>
</>
);
};

interface ChatListProps {
onChatSelected: (e: GraphChat) => void;
chatSelected: GraphChat | undefined;
}

const ChatList = React.memo((props: ChatListProps) => {
const getPreviousDate = (months: number) => {
const date = new Date();
date.setMonth(date.getMonth() - months);
return date.toISOString();
};

return (
<Get
resource={`me/chats?$expand=members,lastMessagePreview&$orderBy=lastMessagePreview/createdDateTime desc&$filter=viewpoint/lastMessageReadDateTime ge ${getPreviousDate(
9
)}`}
scopes={['chat.read']}
cacheEnabled={true}
>
<ChatListTemplate
template="default"
onSelected={props.onChatSelected}
selectedChat={props.chatSelected}
></ChatListTemplate>
<Loading template="loading" message={'Loading your chats...'}></Loading>
</Get>
);
});
161 changes: 161 additions & 0 deletions samples/react-contoso/src/pages/Chats/ChatItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Persona, makeStyles, mergeClasses, shorthands } from '@fluentui/react-components';
import { Providers } from '@microsoft/mgt-element';
import { MgtTemplateProps, Person, ViewType } from '@microsoft/mgt-react';
import { Chat, AadUserConversationMember } from '@microsoft/microsoft-graph-types';
import React, { useCallback, useEffect, useState } from 'react';
import { PeopleCommunityRegular, CalendarMonthRegular } from '@fluentui/react-icons';

const useStyles = makeStyles({
chat: {
paddingLeft: '5px',
paddingRight: '5px',
display: 'flex',
alignItems: 'center',
height: '50px',
cursor: 'pointer',
':hover': {
backgroundColor: 'var(--colorNeutralBackground1Hover)'
}
},
active: {
backgroundColor: 'var(--colorNeutralBackground1Selected)'
},
person: {
'--person-avatar-size-small': '40px',
'& .fui-Persona__primaryText': {
fontSize: 'var(--fontSizeBase300);'
},
'& .fui-Persona__secondaryText': {
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
width: '200px',
display: 'inline-block',
...shorthands.overflow('hidden')
}
},
messagePreview: {
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
width: '200px',
display: 'inline-block',
...shorthands.overflow('hidden')
}
});

export interface ChatInteractionProps {
onSelected: (selected: Chat) => void;
selectedChat?: Chat;
}

interface ChatItemProps {
chat: Chat;
isSelected?: boolean;
}

const getMessagePreview = (chat: Chat) => {
return chat?.lastMessagePreview?.body?.contentType === 'text' ? chat?.lastMessagePreview?.body?.content : '...';
};

const ChatItem = ({ chat, isSelected, onSelected }: ChatItemProps & ChatInteractionProps) => {
const styles = useStyles();
const [myId, setMyId] = useState<string>();

useEffect(() => {
const getMyId = async () => {
const me = await Providers.me();
setMyId(me.id);
};
if (!myId) {
void getMyId();
}
}, [myId]);

const getOtherParticipantId = useCallback(
(chat: Chat) => {
const member = chat.members?.find(m => (m as AadUserConversationMember).userId !== myId);

if (member) {
console.log('member', member);
return (member as AadUserConversationMember).userId as string;
} else if (chat.members?.length === 1 && (chat.members[0] as AadUserConversationMember).userId === myId) {
return myId;
}

return undefined;
},
[myId]
);

const getGroupTitle = useCallback((chat: Chat) => {
let groupTitle: string | undefined = '';
if (chat.topic) {
groupTitle = chat.topic;
} else {
groupTitle = chat.members
?.map(member => {
return member.displayName?.split(' ')[0];
})
.join(', ');
}

return groupTitle;
}, []);

return (
<>
{myId && (
<div className={mergeClasses(styles.chat, `${isSelected && styles.active}`)}>
{chat.chatType === 'oneOnOne' && (
<Person
userId={getOtherParticipantId(chat)}
view={ViewType.twolines}
avatarSize="auto"
showPresence={true}
onClick={() => onSelected(chat)}
className={styles.person}
>
<MessagePreview template="line2" chat={chat} />
</Person>
)}
{chat.chatType === 'group' && (
<div onClick={() => onSelected(chat)}>
<Persona
textAlignment="center"
size="extra-large"
name={getGroupTitle(chat)}
secondaryText={getMessagePreview(chat)}
avatar={{ icon: <PeopleCommunityRegular />, initials: null }}
className={styles.person}
/>
<span></span>
</div>
)}
{chat.chatType === 'meeting' && (
<div onClick={() => onSelected(chat)}>
<Persona
textAlignment="center"
size="extra-large"
className={styles.person}
avatar={{ icon: <CalendarMonthRegular />, initials: null }}
name={getGroupTitle(chat)}
secondaryText={getMessagePreview(chat)}
/>
</div>
)}
</div>
)}
</>
);
};

const MessagePreview = (props: MgtTemplateProps & ChatItemProps) => {
const styles = useStyles();

return (
<>
<span className={styles.messagePreview}>{getMessagePreview(props.chat)}</span>
</>
);
};

export default ChatItem;
Loading

0 comments on commit 3a8e86a

Please sign in to comment.