New Sample react-roomchat

This commit is contained in:
João Mendes 2022-07-19 23:32:19 +01:00
parent fa3a66492d
commit 59fc4754eb
68 changed files with 27673 additions and 0 deletions

View File

@ -0,0 +1,5 @@
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'],
parserOptions: { tsconfigRootDir: __dirname }
};

35
samples/react-roomchat/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
.heft
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts
*.scss.d.ts

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.15.0",
"libraryName": "roomchat",
"libraryId": "4e5de806-161a-4b66-8736-e82ac1edb967",
"environment": "spo",
"packageManager": "npm",
"solutionName": "roomchat",
"solutionShortDescription": "roomchat description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,96 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
- PnPjs
- Fluent UI React Controls
platforms:
- React
createdDate: 19/7/2022 12:00:00 AM
---
# Room Chat
## Summary
This web part shows ao to use Azure Communications Services and React UI component to create a Room Chat.
This is a sample and the arquictecture is very simple for demo only.
![roomchat](./assets/roomchat.gif)
![roomchat](./assets/roomchat.png)
## Requirements
This sample needs the Azurecommunications services configured on the Azure, Please documentation how to configure.
## Compatibility
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Teams Incompatible](https://img.shields.io/badge/Teams-Incompatible-lightgrey.svg)
![Local Workbench Incompatible](https://img.shields.io/badge/Local%20Workbench-Incompatible-red.svg "This solution requires access lists and sites")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react-roomchat|[João Mendes](https://github.com/joaojmendes)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|July 19, 2022|Initial release
## Minimal Path to Awesome
Please follow all the steps:
- Clone this repository
- in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- Add and deploy package to your tenant's App Catalog
## Help
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-restaurant-menu&template=bug-report.yml&sample=react-restaurant-menu&authors=@joaojmendes&title=react-restaurant-menu%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-restaurant-menu&template=question.yml&sample=react-restaurant-menu&authors=@joaojmendes&title=react-restaurant-menu%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-restaurant-menu&template=question.yml&sample=react-restaurant-menu&authors=@joaojmendes&title=react-restaurant-menu%20-%20).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-roomchat" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"room-chat-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/roomChat/RoomChatWebPart.js",
"manifest": "./src/webparts/roomChat/RoomChatWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"RoomChatWebPartStrings": "lib/webparts/roomChat/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "roomchat",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "roomchat-client-side-solution",
"id": "4e5de806-161a-4b66-8736-e82ac1edb967",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.15.0"
},
"metadata": {
"shortDescription": {
"default": "roomchat description"
},
"longDescription": {
"default": "roomchat description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "roomchat Feature",
"description": "The feature that activates elements of the roomchat solution.",
"id": "e63c3fba-3b2b-4ed4-a406-4222e6602a1a",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/roomchat.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/s-KaiNet/spfx-fast-serve/master/schema/config.latest.schema.json",
"cli": {
"isLibraryComponent": false
}
}

View File

@ -0,0 +1,24 @@
/*
* User webpack settings file. You can add your own settings here.
* Changes from this file will be merged into the base webpack configuration file.
* This file will not be overwritten by the subsequent spfx-fast-serve calls.
*/
// you can add your project related webpack configuration here, it will be merged using webpack-merge module
// i.e. plugins: [new webpack.Plugin()]
const webpackConfig = {
}
// for even more fine-grained control, you can apply custom webpack settings using below function
const transformConfig = function (initialWebpackConfig) {
// transform the initial webpack config here, i.e.
// initialWebpackConfig.plugins.push(new webpack.Plugin()); etc.
return initialWebpackConfig;
}
module.exports = {
webpackConfig,
transformConfig
}

22
samples/react-roomchat/gulpfile.js vendored Normal file
View File

@ -0,0 +1,22 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
/* fast-serve */
const { addFastServe } = require("spfx-fast-serve-helpers");
addFastServe(build);
/* end of fast-serve */
build.initialize(require('gulp'));

25202
samples/react-roomchat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "roomchat",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve"
},
"dependencies": {
"@azure/communication-calling": "^1.5.4-beta.1",
"@azure/communication-chat": "^1.2.0",
"@azure/communication-identity": "^1.1.0-beta.2",
"@azure/communication-react": "^1.2.2-beta.1",
"@microsoft/sp-core-library": "1.15.0",
"@microsoft/sp-lodash-subset": "1.15.0",
"@microsoft/sp-office-ui-fabric-core": "1.15.0",
"@microsoft/sp-property-pane": "1.15.0",
"@microsoft/sp-webpart-base": "1.15.0",
"@pnp/graph": "^3.5.1",
"@pnp/logging": "^3.5.1",
"@pnp/sp": "^3.5.1",
"office-ui-fabric-react": "7.185.7",
"react": "16.13.1",
"react-dom": "16.13.1",
"spfx-fast-serve": "^3.0.5",
"tslib": "2.3.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
"@rushstack/eslint-config": "2.5.1",
"@microsoft/eslint-plugin-spfx": "1.15.0",
"@microsoft/eslint-config-spfx": "1.15.0",
"@microsoft/sp-build-web": "1.15.0",
"@types/webpack-env": "~1.15.2",
"ajv": "^6.12.5",
"gulp": "4.0.2",
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"eslint-plugin-react-hooks": "4.3.0",
"@microsoft/sp-module-interfaces": "1.15.0",
"spfx-fast-serve-helpers": "~1.15.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,59 @@
/* eslint-disable react/jsx-no-bind */
import * as React from 'react';
import { Stack } from 'office-ui-fabric-react';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { IUserIdentity } from '../../models/IUserIdentity';
import {
BOY,
CAT,
FOX,
GIRL,
KOALA,
MAN,
MONKEY,
MOUSE,
OCTOPUS,
WOMAN,
} from '../../utils/avatars';
import { useAvatarsStyles } from './useAvatarsStyles';
export interface IAvatarsProps {
onSelect: (avatar: string) => void;
}
export const Avatars: React.FunctionComponent<IAvatarsProps> = (props: React.PropsWithChildren<IAvatarsProps>) => {
const avatars: string[] = [CAT, MOUSE, FOX, KOALA, OCTOPUS, MONKEY, GIRL, BOY, MAN, WOMAN];
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { useIdentity } = GlobalState ;
const { avatar } = useIdentity || {} as IUserIdentity;
const { onSelect } = props;
const { avatarControlStyles, avatarContainerStyle } = useAvatarsStyles();
return (
<>
<Stack horizontal horizontalAlign="center" tokens={{ childrenGap: 5 }} wrap role="list">
{avatars.map((avatarElement, index) => {
return (
<div
role="listitem"
id={avatarElement}
key={index}
data-is-focusable={true}
className={avatarContainerStyle(avatarElement, avatar)}
onClick={() => onSelect(avatarElement)}
>
<div data-is-focusable={true} className={avatarControlStyles.smallAvatarStyle} key={index}>
{avatarElement}
</div>
</div>
);
})}
</Stack>
</>
);
};

View File

@ -0,0 +1,64 @@
import * as React from 'react';
import {
IProcessedStyleSet,
mergeStyles,
mergeStyleSets,
} from '@uifabric/merge-styles';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { getBackgroundColor } from '../../utils/avatars';
interface IAvatarsStyles {
avatarControlStyles: IProcessedStyleSet<{
smallAvatarStyle: string;
}>;
avatarContainerStyle: (avatar: string, selectedAvatar: string) => string;
}
export const useAvatarsStyles = (): IAvatarsStyles => {
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { theme } = GlobalState;
const avatarControlStyles: IProcessedStyleSet<{
smallAvatarStyle: string;
}> = React.useMemo(() => mergeStyleSets({
smallAvatarStyle: mergeStyles({
height: "1.75rem",
width: "2rem",
color: "#444444",
fontWeight: 400,
fontSize: "1.5rem",
letterSpacing: "0",
lineHeight: "1.75rem",
textAlign: "center",
cursor: "pointer",
}),
}), []);
const avatarContainerStyle: (avatar: string, selectedAvatar: string) => string = React.useCallback(
(avatar: string, selectedAvatar: string): string =>
mergeStyles({
padding: 3,
borderRadius: "50%",
alignItems: "center",
display: "flex",
justifyContent: "center",
outline: "none",
backgroundColor: `${getBackgroundColor(avatar).backgroundColor}`,
border: `2px solid ${
avatar === selectedAvatar ? theme?.palette?.neutralSecondaryAlt : theme?.palette?.neutralQuaternaryAlt
}`,
"&:hover": {
border: `2px solid ${ avatar === selectedAvatar ? theme?.palette?.neutralSecondaryAlt : theme?.palette?.neutralQuaternary}`,
borderRadius: "50%",
},
}),
[theme]
);
return { avatarControlStyles, avatarContainerStyle };
};

View File

@ -0,0 +1,144 @@
/* eslint-disable no-empty */
/* eslint-disable react/jsx-no-bind */
/* eslint-disable @microsoft/spfx/no-async-await */
/* eslint-disable @typescript-eslint/no-floating-promises */
import * as React from 'react';
import {
ActionButton,
MessageBarType,
SpinnerSize,
Stack,
} from 'office-ui-fabric-react';
import * as strings from 'RoomChatWebPartStrings';
import {
AvatarPersonaData,
ChatAdapter,
ChatComposite,
} from '@azure/communication-react';
import { isEmpty } from '@microsoft/sp-lodash-subset';
import { EProcessingStatus } from '../../constants/EProcessingStatus';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { useAcsApi } from '../../hooks/useAcsApi';
import { IErrorInfo } from '../../models/IErrorInfo';
import { IUserIdentity } from '../../models/IUserIdentity';
import { getBackgroundColor } from '../../utils/avatars';
import { useUtils } from '../../utils/useUtils';
import { useChatPanelStyles } from '../ChatPanel/useChatPanelStyles';
import { Message } from '../Message/message';
import { Spinner } from '../Spinner/Spinner';
import { ShowParticipantsButton } from './ShowParticipantsButton';
export interface IChatProps {
onLeaveChat: () => void;
}
export const Chat: React.FunctionComponent<IChatProps> = (props: React.PropsWithChildren<IChatProps>) => {
const { onLeaveChat } = props;
const { stackChatContainer } = useChatPanelStyles();
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { useIdentity, chatThreadId, context, theme } = GlobalState;
const chatAdapterRef: React.MutableRefObject<ChatAdapter> = React.useRef<ChatAdapter>(null);
const [chatAdapter, setChatAdapter] = React.useState<ChatAdapter>(null);
const { createAdapter } = useAcsApi();
const { userId, displayName, userCredential } = useIdentity || ({} as IUserIdentity);
const [processingStatus, setProcessingStatus] = React.useState<EProcessingStatus>(EProcessingStatus.Loading);
const [errorInfo, setErrorInfo] = React.useState<IErrorInfo>({ hasError: false, error: undefined });
const [showParticipants, setShowParticipants] = React.useState(true);
const { getChatParticipantFromSupportList, removeChatParticipantFromSupportList } = useUtils();
const onFetchAvatarPersonaData = (userId: string): Promise<AvatarPersonaData> =>
getChatParticipantFromSupportList(context.instanceId, userId).then((userInfo) => {
return new Promise((resolve) => {
return resolve({
imageInitials: userInfo?.avatar,
initialsColor: userInfo?.avatar ? getBackgroundColor(userInfo?.avatar)?.backgroundColor : "#0078d4",
imageUrl: !userInfo?.avatar ? `/_layouts/15/userphoto.aspx?size=L&username=${userInfo?.email}` : undefined,
});
});
});
React.useEffect(() => {
(async () => {
try {
const chatAdapter: ChatAdapter = await createAdapter(displayName, userId, userCredential, chatThreadId);
chatAdapterRef.current = chatAdapter;
setChatAdapter(chatAdapter);
setProcessingStatus(EProcessingStatus.Done);
setErrorInfo({ hasError: false, error: undefined });
} catch (error) {
setProcessingStatus(EProcessingStatus.Error);
setErrorInfo({ hasError: true, error });
setProcessingStatus(EProcessingStatus.Done);
}
})();
return () => {
chatAdapterRef.current?.dispose();
};
}, [chatThreadId, createAdapter, displayName, userCredential, userId]);
const onLeaveClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(
async (ev) => {
ev.preventDefault();
chatAdapter.removeParticipant(userId);
await removeChatParticipantFromSupportList(context.instanceId, userId);
onLeaveChat();
},
[chatAdapter, context.instanceId, onLeaveChat, removeChatParticipantFromSupportList, userId]
);
const onShowParticipants: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(
(ev) => {
ev.preventDefault();
setShowParticipants(!showParticipants);
},
[showParticipants]
);
if (isEmpty(useIdentity)) return null;
if (processingStatus === EProcessingStatus.Error) {
const { error } = errorInfo;
return (
<Message showMessage={true} message={""} msgType={MessageBarType.error}>
{error.message}
</Message>
);
}
if (processingStatus === EProcessingStatus.Loading) {
return <Spinner showSpinner={true} size={SpinnerSize.medium} />;
}
if (processingStatus === EProcessingStatus.Done && chatAdapter) {
return (
<>
<Stack horizontal horizontalAlign="end" tokens={{ childrenGap: 10 }}>
<ActionButton iconProps={{ iconName: "Leave" }} allowDisabledFocus onClick={onLeaveClick}>
{strings.LeaveChatLabel}
</ActionButton>
<ShowParticipantsButton showParticipants={showParticipants} onClick={onShowParticipants} />
</Stack>
<Stack tokens={{ childrenGap: 10 }} styles={stackChatContainer}>
<ChatComposite
adapter={chatAdapter}
fluentTheme={{...theme}}
options={{
errorBar: true,
participantPane: showParticipants,
}}
onFetchAvatarPersonaData={onFetchAvatarPersonaData}
/>
</Stack>
</>
);
}
return null;
};

View File

@ -0,0 +1,31 @@
import * as React from 'react';
import {
ActionButton,
IIconProps,
} from 'office-ui-fabric-react';
export interface IShowParticipantsProps {
showParticipants: boolean;
onClick : React.MouseEventHandler<HTMLButtonElement>
}
export const ShowParticipantsButton: React.FunctionComponent<IShowParticipantsProps> = (
props: React.PropsWithChildren<IShowParticipantsProps>
) => {
const { showParticipants, onClick } = props;
const contactListIcon: IIconProps = React.useMemo(() => {
return { iconName: "ContactList" };
}, []);
const lbuttonLabel: string = React.useMemo(() => {
return showParticipants ?"Hide participants" : "Show participants" ;
}, [showParticipants]);
return (
<>
<ActionButton iconProps={contactListIcon} allowDisabledFocus onClick={ onClick }>
{lbuttonLabel}
</ActionButton>
</>
);
};

View File

@ -0,0 +1,64 @@
/* eslint-disable @typescript-eslint/typedef */
import * as React from 'react';
import {
Panel,
PanelType,
Stack,
} from 'office-ui-fabric-react';
import { EScreens } from '../../constants/EScreens';
import {
EActionTypes,
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { Chat } from '../Chat/Chat';
import { IChatPanelProps } from './IChatPanelProps';
import { useChatPanelStyles } from './useChatPanelStyles';
export const ChatPanel: React.FunctionComponent<IChatPanelProps> = (
props: React.PropsWithChildren<IChatPanelProps>
) => {
const { GlobalState, setGlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { startChat } = GlobalState || {};
const { panelStyles } = useChatPanelStyles();
const onDismiss: (ev?: React.SyntheticEvent<HTMLElement, Event> | KeyboardEvent) => void = React.useCallback(
(ev): void => {
setGlobalState({
type: EActionTypes.SET_SHOW_SCREEN,
payload: EScreens.RoomChatJoin,
});
setGlobalState({
type: EActionTypes.SET_START_CHAT,
payload: false,
});
},
[setGlobalState]
);
return (
<>
<Stack>
<Panel
isOpen={startChat}
onDismiss={onDismiss}
hasCloseButton={false}
type={PanelType.large}
styles={panelStyles}
>
<Stack tokens={{ childrenGap: 20, padding: 10 }} styles={{root:{height: `95%`, paddindBottom: 20}}}>
<Chat onLeaveChat={onDismiss}/>
</Stack>
</Panel>
</Stack>
</>
);
};

View File

@ -0,0 +1,4 @@
export interface IChatPanelProps {
}

View File

@ -0,0 +1,23 @@
import {
IPanelStyles,
IStackStyles,
} from 'office-ui-fabric-react';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useChatPanelStyles = () => {
const stackChatContainer: IStackStyles = {
root: {
width: "100%",
border: "1px solid #ccc",
height: "100%",
paddingBottom: 20,
},
};
const panelStyles: Partial<IPanelStyles> = {
content: { height: "100%" },
scrollableContent: { height: "100%" },
};
return { stackChatContainer, panelStyles };
};

View File

@ -0,0 +1,192 @@
/* eslint-disable @microsoft/spfx/no-async-await */
import * as react from 'react';
import * as React from 'react';
import {
DefaultButton,
Dialog,
DialogFooter,
DialogType,
IDialogContentProps,
IModalProps,
PrimaryButton,
SpinnerSize,
Stack,
Text,
TextField,
} from 'office-ui-fabric-react';
import * as strings from 'RoomChatWebPartStrings';
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
import { EScreens } from '../../constants/EScreens';
import {
EActionTypes,
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { useAcsApi } from '../../hooks';
import { IUserIdentity } from '../../models/IUserIdentity';
import { useUtils } from '../../utils/useUtils';
import { Avatars } from '../Avatars/Avatars';
import { Spinner } from '../Spinner/Spinner';
import { useJoinUserStyles } from './useJoinUserStyles';
export interface IJoinUserProps {
isOpen: boolean;
onDismiss: () => void;
}
export const JoinUser: React.FunctionComponent<IJoinUserProps> = (props: React.PropsWithChildren<IJoinUserProps>) => {
const { GlobalState, setGlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { isOpen, onDismiss } = props;
const { topic, context, chatThreadId, useIdentity, theme } = GlobalState;
const { displayName, email } = context.pageContext.user;
const { dialogContentStyles, modalStyles, textAvatarLabelStyles } = useJoinUserStyles();
const [userName, setUserName] = React.useState(displayName);
const { addParticipantsToChatThreadClient, getCredentials, getUserIdentity } = useAcsApi();
const [isRunning, setIsRunning] = React.useState(false);
const { saveChatParticipantToSupportList } = useUtils();
const modelProps: IModalProps = react.useMemo(() => {
return {
isBlocking: true,
styles: modalStyles,
};
}, [modalStyles]);
const dialogContentProps: IDialogContentProps = React.useMemo(() => {
return {
type: DialogType.largeHeader,
title: `${strings.DialogTitleLabel} ${topic}`,
subText: strings.DialogSubTitleLabel,
styles: dialogContentStyles,
};
}, [dialogContentStyles, topic]);
react.useEffect(() => {
setGlobalState({
type: EActionTypes.SET_USER_INFO,
payload: {
...useIdentity,
displayName: userName,
} as IUserIdentity,
});
}, [userName]);
const onChange: (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void = React.useCallback(
(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
ev.preventDefault();
setUserName(newValue);
},
[]
);
const hasUserName: boolean = react.useMemo(() => {
return userName.length > 0;
}, [userName]);
const onSelectAvatar: (avatar: string) => void = React.useCallback(
(avatar: string): void => {
setGlobalState({
type: EActionTypes.SET_USER_INFO,
payload: {
...useIdentity,
displayName: userName,
avatar: avatar,
} as IUserIdentity,
});
},
[setGlobalState, useIdentity, userName]
);
const onJoin: () => void = React.useCallback(async () => {
let participantUserId: string;
let participantDisplayName: string;
const { userId, displayName } = useIdentity;
setIsRunning(true);
if (!userId) {
const userIdentity: IUserIdentity = await getUserIdentity();
const { userId, accessToken } = userIdentity;
const userCredential: AzureCommunicationTokenCredential = await getCredentials(accessToken);
participantUserId = userId;
participantDisplayName = displayName;
const userInfo: IUserIdentity = {
...useIdentity,
userId: userId,
accessToken: accessToken,
userCredential: userCredential,
threadId: chatThreadId,
email: email,
} as IUserIdentity;
setGlobalState({
type: EActionTypes.SET_USER_INFO,
payload: userInfo,
});
await saveChatParticipantToSupportList(context.instanceId, userInfo);
} else {
participantUserId = userId;
participantDisplayName = displayName;
await saveChatParticipantToSupportList(context.instanceId, useIdentity);
}
await addParticipantsToChatThreadClient(participantUserId, participantDisplayName);
setIsRunning(false);
setGlobalState({
type: EActionTypes.SET_SHOW_SCREEN,
payload: EScreens.RoomChatJoin,
});
setGlobalState({
type: EActionTypes.SET_START_CHAT,
payload: true,
});
onDismiss();
}, [
addParticipantsToChatThreadClient,
chatThreadId,
context.instanceId,
email,
getCredentials,
getUserIdentity,
onDismiss,
saveChatParticipantToSupportList,
setGlobalState,
useIdentity,
]);
return (
<>
<Dialog hidden={!isOpen} onDismiss={onDismiss} dialogContentProps={dialogContentProps} modalProps={modelProps}>
<Stack tokens={{ childrenGap: 10 }} style={{ padding: 0 }}>
<Text variant="medium" styles={textAvatarLabelStyles}>
Avatar
</Text>
<Avatars onSelect={onSelectAvatar} />
<TextField value={userName} label={"Name"} onChange={onChange} placeholder={strings.EnterNameLabel} />
</Stack>
<DialogFooter>
<PrimaryButton onClick={onJoin} disabled={!hasUserName}>
{isRunning ? (
<Spinner
size={SpinnerSize.xSmall}
showSpinner={isRunning}
styles={{ root: { color: theme.palette.white } }}
/>
) : (
"Join"
)}
</PrimaryButton>
<DefaultButton onClick={onDismiss} text={strings.BurttonLabelCancel} />
</DialogFooter>
</Dialog>
</>
);
};

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import {
IDialogContentStyles,
IModalStyles,
ITextStyles,
} from 'office-ui-fabric-react';
interface IJoinUserStyles {
dialogContentStyles: IDialogContentStyles;
modalStyles: IModalStyles;
textAvatarLabelStyles: ITextStyles;
}
export const useJoinUserStyles = (): IJoinUserStyles => {
const dialogContentStyles: IDialogContentStyles = React.useMemo(() => {
return {
subText: { marginBottom: 10 },
} as IDialogContentStyles;
}, []);
const modalStyles: IModalStyles = React.useMemo(() => {
return {
main: { maxWidth: 450 },
} as IModalStyles;
}, []);
const textAvatarLabelStyles: ITextStyles = React.useMemo(() => {
return {
root: { fontWeight: 600 },
} as ITextStyles;
}, []);
return { dialogContentStyles, modalStyles, textAvatarLabelStyles };
};

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import {
MessageBar,
MessageBarType,
} from 'office-ui-fabric-react';
export interface IMessageProps {
showMessage: boolean;
message: string | JSX.Element
msgType: MessageBarType;
}
export const Message: React.FunctionComponent<IMessageProps> = (props: React.PropsWithChildren<IMessageProps>) => {
const { showMessage, message, msgType } = props;
if (showMessage) {
return (
<MessageBar messageBarType={msgType ?? MessageBarType.info} isMultiline>
{message}
</MessageBar>
)
}
return null;
};

View File

@ -0,0 +1,34 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.acsChat {
overflow: hidden;
padding: 1em;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
&.teams {
font-family: $ms-font-family-fallbacks;
}
}
.welcome {
text-align: center;
}
.welcomeImage {
width: 100%;
max-width: 420px;
}
.links {
a {
text-decoration: none;
color: "[theme:link, default:#03787c]";
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
&:hover {
text-decoration: underline;
color: "[theme:linkHovered, default: #014446]";
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
}
}
}

View File

@ -0,0 +1,10 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { WebPartContext } from '@microsoft/sp-webpart-base';
export interface IRoomChatProps {
topic: string;
isDarkTheme: boolean;
theme: IReadonlyTheme | undefined;
context: WebPartContext;
acsConnectString: string;
}

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import { Customizer } from 'office-ui-fabric-react';
import { GlobalStateProvider } from '../../globalStateProvider';
import { IRoomChatProps } from './IRoomChatProps';
import { RoomChatControl } from './RoomChatControl';
export const RoomChat: React.FunctionComponent<IRoomChatProps> = (props: React.PropsWithChildren<IRoomChatProps>) => {
return (
<>
<Customizer settings={{ theme: props.theme }}>
<GlobalStateProvider>
<RoomChatControl {...props} />
</GlobalStateProvider>
</Customizer>
</>
);
};

View File

@ -0,0 +1,98 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @microsoft/spfx/no-async-await */
import * as React from 'react';
import {
Spinner,
SpinnerSize,
} from 'office-ui-fabric-react';
import { ChatThreadClient } from '@azure/communication-chat';
import { EProcessingStatus } from '../../constants/EProcessingStatus';
import { EScreens } from '../../constants/EScreens';
import {
EActionTypes,
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { useAcsApi } from '../../hooks';
import { IChatModeratorInfo } from '../../models/IChatModeratorInfo';
import { IErrorInfo } from '../../models/IErrorInfo';
import { ChatPanel } from '../ChatPanel/ChatPanel';
import { RoomChatConfig } from '../RoomChatConfig/RoomChatConfig';
import { RoomChatError } from '../RoomChatError/RoomChatError';
import { RoomChatJoin } from '../RoomChatJoin/RoomChatJoin';
import { IRoomChatProps } from './IRoomChatProps';
export const RoomChatControl: React.FunctionComponent<IRoomChatProps> = (
props: React.PropsWithChildren<IRoomChatProps>
) => {
const { theme, topic, context, acsConnectString } = props;
const { GlobalState, setGlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const [processingStatus, setProcessingStatus] = React.useState<EProcessingStatus>(EProcessingStatus.Loading);
const { moderatorInfo } = GlobalState;
const { threadId, moderatorAccessToken } = moderatorInfo || ({} as IChatModeratorInfo);
const { getCurrentThreadById } = useAcsApi();
React.useEffect(() => {
setGlobalState({
type: EActionTypes.SET_CONTEXT,
payload: context,
});
setGlobalState({
type: EActionTypes.SET_THEME,
payload: theme,
});
setProcessingStatus(EProcessingStatus.Done);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
setGlobalState({
type: EActionTypes.SET_ACS_CONNECT_STRING,
payload: acsConnectString,
});
setGlobalState({
type: EActionTypes.SET_SHOW_SCREEN,
payload: acsConnectString ? EScreens.RoomChatJoin : EScreens.RoomChatConfig,
});
setProcessingStatus(EProcessingStatus.Done);
}, [acsConnectString, setGlobalState]);
React.useEffect(() => {
try {
if (threadId && moderatorAccessToken ) {
const currentClient: ChatThreadClient = getCurrentThreadById(threadId, moderatorAccessToken);
currentClient.updateTopic(topic);
}
setGlobalState({
type: EActionTypes.SET_TOPIC,
payload: topic,
});
} catch (error) {
if (DEBUG){
console.log('[RoomChatControl] Error updating topic: ', error);
}
setGlobalState({
type: EActionTypes.SET_ERROR_INFO,
payload: { hasError: true, error } as IErrorInfo,
});
}
}, [getCurrentThreadById, moderatorAccessToken, setGlobalState, threadId, topic]);
if (processingStatus === EProcessingStatus.Loading) {
return <Spinner size={SpinnerSize.medium} />;
}
return (
<>
<RoomChatError />
<RoomChatJoin />
<RoomChatConfig />
<ChatPanel />
</>
);
};

View File

@ -0,0 +1,3 @@
export * from './AcsChat.module.scss';
export * from './RoomChatControl';
export * from './IRoomChatProps';

View File

@ -0,0 +1,74 @@
import * as react from 'react';
import {
FontSizes,
FontWeights,
IDocumentCardPreviewStyles,
IDocumentCardStyles,
IIconStyles,
IStackStyles,
ITextStyles,
} from 'office-ui-fabric-react';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useRoomChatStyles = ( ) => {
const { GlobalState } = react.useContext<IGlobalStateContext>(GlobalStateContext);
const { theme } = GlobalState;
const previewIconStyles: Partial<IDocumentCardPreviewStyles> = react.useMemo(() => {
return {
previewIcon: { backgroundColor: theme?.palette.neutralLighter },
};
}, [theme]);
const documentCardStyles: IDocumentCardStyles = react.useMemo(() => {
return {
root: {
maxWidth: 320,
maxHeight: 106,
minHeight: 106,
height: '100%',
width: '100%',
},
};
}, []);
const stackContainerStyles: IStackStyles = react.useMemo(() => {
return {
root: {
paddingTop: 10,
paddingLeft:15,
paddingRight: 15,
paddingBottom: 10,
},
};
}, []);
const textStyles: ITextStyles = react.useMemo(() => {
return {
root: {
color: theme?.palette?.themePrimary,
fontWeight: FontWeights.semibold,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
textAlign: "start",
},
};
}, [theme]);
const iconStyles: IIconStyles = react.useMemo(() => {
return {
root: { fontSize: FontSizes.superLarge, color: theme?.palette?.themePrimary },
};
}, [theme]);
return { previewIconStyles, documentCardStyles, stackContainerStyles, textStyles, iconStyles };
};

View File

@ -0,0 +1,79 @@
import * as React from 'react';
import {
DocumentCard,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardType,
IDocumentCardPreviewProps,
PrimaryButton,
Stack,
Text,
} from 'office-ui-fabric-react';
import * as strings from 'RoomChatWebPartStrings';
import { DisplayMode } from '@microsoft/sp-core-library';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { EScreens } from '../../constants/EScreens';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { useRoomChatStyles } from '../RoomChat/useRoomChatStyles';
export interface IRoomChatConfigProps {
}
export const RoomChatConfig: React.FunctionComponent<IRoomChatConfigProps> = (
props: React.PropsWithChildren<IRoomChatConfigProps>
) => {
const { documentCardStyles, stackContainerStyles, textStyles, previewIconStyles, iconStyles } = useRoomChatStyles();
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const {showScreen, context } = GlobalState;
const onClick: React.MouseEventHandler<HTMLDivElement | HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement > = React.useCallback((ev) => {
ev.preventDefault();
(context as WebPartContext).propertyPane.open();
}, [context]);
const previewPropsUsingIcon: IDocumentCardPreviewProps = React.useMemo(() => {
return {
previewImages: [
{
previewIconProps: {
iconName: "ChatInviteFriend",
styles: iconStyles,
},
width: 110,
},
],
styles: previewIconStyles,
};
}, [iconStyles, previewIconStyles]);
if (showScreen === EScreens.RoomChatConfig) {
return (
<>
<Stack tokens={{ childrenGap: 10 }} horizontalAlign={"center"}>
<DocumentCard aria-label="Room Chat Config" type={DocumentCardType.compact} styles={documentCardStyles}>
<DocumentCardPreview {...previewPropsUsingIcon} />
<DocumentCardDetails>
<Stack horizontalAlign="center" tokens={{ childrenGap: 15 }} styles={stackContainerStyles}>
<Text variant="mediumPlus" styles={textStyles}>
{strings.ConfigureMessageLabel}
</Text>
{DisplayMode.Edit && ( <PrimaryButton onClick={onClick}>{strings.ButtonLabelConfigure}</PrimaryButton>)}
</Stack>
</DocumentCardDetails>
</DocumentCard>
</Stack>
</>
);
}
return null;
};

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import {
MessageBarType,
Stack,
} from 'office-ui-fabric-react';
import {
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { IErrorInfo } from '../../models/IErrorInfo';
import { Message } from '../Message/message';
export interface IRoomChatErrorProps {}
export const RoomChatError: React.FunctionComponent<IRoomChatErrorProps> = () => {
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { errorInfo } = GlobalState;
const { hasError, error } = errorInfo || ({ hasError: false, error: null } as IErrorInfo);
if (hasError) {
return (
<>
<Stack tokens={{ childrenGap: 10 }} horizontalAlign={"center"}>
<Message message={error.message} msgType={MessageBarType.error} showMessage={true} />
</Stack>
</>
);
}
return null;
};

View File

@ -0,0 +1,124 @@
/* eslint-disable @microsoft/spfx/no-async-await */
/* eslint-disable @typescript-eslint/no-floating-promises */
import * as React from 'react';
import {
DocumentCard,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardType,
IDocumentCardPreviewProps,
PrimaryButton,
Stack,
Text,
} from 'office-ui-fabric-react';
import * as strings from 'RoomChatWebPartStrings';
import { EScreens } from '../../constants/EScreens';
import {
EActionTypes,
GlobalStateContext,
IGlobalStateContext,
} from '../../globalStateProvider';
import { useAcsApi } from '../../hooks';
import { IChatModeratorInfo } from '../../models/IChatModeratorInfo';
import { IErrorInfo } from '../../models/IErrorInfo';
import { JoinUser } from '../JoinUser/JoinUser';
import { useRoomChatStyles } from '../RoomChat/useRoomChatStyles';
export interface IRoomChatJoinProps {}
export const RoomChatJoin: React.FunctionComponent<IRoomChatJoinProps> = (
) => {
const { GlobalState, setGlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { topic, showScreen, acsConnectString } = GlobalState;
const { documentCardStyles, stackContainerStyles, textStyles, previewIconStyles, iconStyles } = useRoomChatStyles();
const { createChatThread } = useAcsApi();
const [openJoinUserDialog, setOpenJoinUserDialog] = React.useState<boolean>(false);
const previewPropsUsingIcon: IDocumentCardPreviewProps = React.useMemo(() => {
return {
previewImages: [
{
previewIconProps: {
iconName: "ChatInviteFriend",
styles: iconStyles,
},
width: 110,
},
],
styles: previewIconStyles,
};
}, [iconStyles, previewIconStyles]);
React.useEffect(() => {
(async () => {
if (acsConnectString?.trim().length > 0) {
try {
setGlobalState({
type: EActionTypes.SET_ERROR_INFO,
payload: { hasError: false, error: undefined } as IErrorInfo,
});
const chatModeratorInfo: IChatModeratorInfo = await createChatThread();
setGlobalState({
type: EActionTypes.SET_CHAT_THREAD_ID,
payload: chatModeratorInfo?.threadId ?? "",
});
setGlobalState({
type: EActionTypes.SET_MODERATOR_INFO,
payload: chatModeratorInfo,
});
} catch (error) {
setGlobalState({
type: EActionTypes.SET_ERROR_INFO,
payload: { hasError: true, error } as IErrorInfo,
});
setGlobalState({
type: EActionTypes.SET_SHOW_SCREEN,
payload: EScreens.RoomChatError,
});
console.error(error);
}
} else {
setGlobalState({
type: EActionTypes.SET_SHOW_SCREEN,
payload: EScreens.RoomChatConfig,
});
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [acsConnectString]);
const onClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(() => {
setOpenJoinUserDialog(true);
}, []);
const onDismiss: () => void = React.useCallback(() => {
setOpenJoinUserDialog(false);
}, []);
if (showScreen === EScreens.RoomChatJoin) {
return (
<>
<Stack tokens={{ childrenGap: 10 }} horizontalAlign={"center"}>
<DocumentCard aria-label="Room Chat" type={DocumentCardType.compact} styles={documentCardStyles}>
<DocumentCardPreview {...previewPropsUsingIcon} />
<DocumentCardDetails>
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }} styles={stackContainerStyles}>
<Text variant="mediumPlus" styles={textStyles}>
{topic}
</Text>
<PrimaryButton onClick={onClick}>{strings.ButtonLabelJoin}</PrimaryButton>
</Stack>
</DocumentCardDetails>
</DocumentCard>
</Stack>
<JoinUser isOpen={openJoinUserDialog} onDismiss={onDismiss} />
</>
);
}
return null;
};

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import {
ISpinnerStyles,
Spinner as SpinnerFUI,
SpinnerSize,
Stack,
} from 'office-ui-fabric-react';
export interface ISpinnerProps {
showSpinner: boolean;
size: SpinnerSize;
styles?: ISpinnerStyles;
}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
const { showSpinner, size, styles } = props;
if (showSpinner) {
return (
<Stack horizontal verticalAlign="center" horizontalAlign="center" style={{ width: "100%" }}>
<SpinnerFUI size={size} styles={styles} />
</Stack>
);
}
return null;
};

View File

@ -0,0 +1,6 @@
export enum EProcessingStatus {
Loading = "loading",
Done = "done",
Error = "error",
idle = "idle",
}

View File

@ -0,0 +1,7 @@
export enum EScreens {
RoomChat = "RoomChat",
RoomChatConfig = "RoomChatConfig",
RoomChatError = "RoomChatError",
RoomChatJoin = "RoomChatJoin",
JoinUser = "JoinUser",
}

View File

@ -0,0 +1,4 @@
export const ENDPOINT_URL:string = 'https://comunicationsservices.communication.azure.com';
export const SUPPORT_LIST:string = "RoomChatSupportList";
export const CHAT_KEY_ID:string = `_chatThreadId`;
export const CHAT_KEY_PARTICIPANTES:string = `_participants`;

View File

@ -0,0 +1 @@
export * from './constants';

View File

@ -0,0 +1,14 @@
export enum EActionTypes {
"SET_PROCESSING_STATUS" = "SET_PROCESSING_STATUS",
"SET_CONTEXT" = "SET_CONTEXT",
"SET_USER_INFO" = "SET_USER_INFO",
"SET_MODERATOR_INFO" = "SET_MODERATOR_INFO",
"SET_THEME" = "SET_THEME",
"SET_TOPIC" = "SET_TOPIC",
"SET_ACS_CONNECT_STRING" = "SET_ACS_CONNECT_STRING",
"SET_CHAT_THREAD_ID" = "SET_CHAT_THREAD_ID",
"SET_SHOW_SCREEN" = "SET_SHOW_SCREEN",
"SET_ERROR_INFO" = "SET_ERROR_INFO",
"SET_START_CHAT" = "SET_START_CHAT",
"SET_UPDATE_STATE" = "SET_UPDATE_STATE",
}

View File

@ -0,0 +1,41 @@
import * as React from 'react';
import {
createContext,
useReducer,
} from 'react';
import { EProcessingStatus } from '../constants/EProcessingStatus';
import { EScreens } from '../constants/EScreens';
import { IGlobalStateContext } from './IGlobalStateContext';
import { IState } from './IState';
import { Reducer } from './Reducer';
const initialState: IState = {
processingStatus: EProcessingStatus.idle,
context: undefined,
useIdentity: undefined,
moderatorInfo: undefined,
theme: undefined,
topic : undefined,
acsConnectString: undefined,
chatThreadId: undefined,
showScreen: EScreens.RoomChat,
errorInfo: undefined,
startChat: false,
};
const stateInit: IGlobalStateContext = {
GlobalState: initialState,
setGlobalState: (() => { return; })
};
// Create Context
export const GlobalStateContext:React.Context<IGlobalStateContext> = createContext<IGlobalStateContext>(stateInit);
// Global State Provider
export const GlobalStateProvider = (props: { children: React.ReactNode }): JSX.Element => {
const [GlobalState, setGlobalState] = useReducer(Reducer, initialState);
return (
<GlobalStateContext.Provider value={{GlobalState, setGlobalState }}>
{props.children}
</GlobalStateContext.Provider>
);
};

View File

@ -0,0 +1,7 @@
import { EActionTypes } from './EActionTypes';
import { IState } from './IState';
export interface IGlobalStateContext {
GlobalState: IState;
setGlobalState: React.Dispatch<{type:EActionTypes, payload: unknown}>;
}

View File

@ -0,0 +1,28 @@
import {
IPartialTheme,
ITheme,
} from 'office-ui-fabric-react';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { EProcessingStatus } from '../constants/EProcessingStatus';
import { EScreens } from '../constants/EScreens';
import { IChatModeratorInfo } from '../models/IChatModeratorInfo';
import { IErrorInfo } from '../models/IErrorInfo';
import { IUserIdentity } from '../models/IUserIdentity';
export interface IState {
context: WebPartContext;
processingStatus: EProcessingStatus;
useIdentity:IUserIdentity;
moderatorInfo: IChatModeratorInfo;
theme: IReadonlyTheme |ITheme | IPartialTheme | undefined;
topic:string;
acsConnectString:string;
chatThreadId:string;
showScreen:EScreens;
errorInfo: IErrorInfo;
startChat:boolean;
}

View File

@ -0,0 +1,43 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { EProcessingStatus } from '../constants/EProcessingStatus';
import { EScreens } from '../constants/EScreens';
import { IChatModeratorInfo } from '../models/IChatModeratorInfo';
import { IErrorInfo } from '../models/IErrorInfo';
import { IUserIdentity } from '../models/IUserIdentity';
import { EActionTypes } from './EActionTypes';
import { IState } from './IState';
// Reducer
export const Reducer = (state: IState, action: { type: EActionTypes; payload: unknown }): IState => {
switch (action.type) {
case EActionTypes.SET_PROCESSING_STATUS:
return { ...state, processingStatus: action.payload as EProcessingStatus };
case EActionTypes.SET_CONTEXT:
return { ...state, context: action.payload as WebPartContext };
case EActionTypes.SET_USER_INFO:
return { ...state, useIdentity: action.payload as IUserIdentity };
case EActionTypes.SET_MODERATOR_INFO:
return { ...state, moderatorInfo: action.payload as IChatModeratorInfo };
case EActionTypes.SET_TOPIC:
return { ...state, topic: action.payload as string };
case EActionTypes.SET_THEME:
return { ...state, theme: action.payload as IReadonlyTheme };
case EActionTypes.SET_ACS_CONNECT_STRING:
return { ...state, acsConnectString: action.payload as string };
case EActionTypes.SET_CHAT_THREAD_ID:
return { ...state, chatThreadId: action.payload as string };
case EActionTypes.SET_SHOW_SCREEN:
return { ...state, showScreen: action.payload as EScreens };
case EActionTypes.SET_ERROR_INFO:
return { ...state, errorInfo: action.payload as IErrorInfo };
case EActionTypes.SET_START_CHAT:
return { ...state, startChat: action.payload as boolean };
case EActionTypes.SET_UPDATE_STATE:
return { ...(action.payload as IState) };
default:
return state;
}
};

View File

@ -0,0 +1,5 @@
export * from './EActionTypes';
export * from './IGlobalStateContext';
export * from './IState';
export * from './GlobalStateProvider';
export * from './Reducer';

View File

@ -0,0 +1 @@
export * from './useAcsApi';

View File

@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @microsoft/spfx/no-async-await */
/* eslint-disable @typescript-eslint/typedef */
import * as React from 'react';
import {
ChatClient,
ChatThreadClient,
} from '@azure/communication-chat';
import {
AzureCommunicationTokenCredential,
CommunicationUserIdentifier,
} from '@azure/communication-common';
import {
CommunicationAccessToken,
CommunicationIdentityClient,
} from '@azure/communication-identity';
import {
ChatAdapter,
createAzureCommunicationChatAdapter,
fromFlatCommunicationIdentifier,
} from '@azure/communication-react';
import { ENDPOINT_URL } from '../constants';
import {
EActionTypes,
GlobalStateContext,
IGlobalStateContext,
IState,
} from '../globalStateProvider';
import { IChatModeratorInfo } from '../models/IChatModeratorInfo';
import { IErrorInfo } from '../models/IErrorInfo';
import { IUserIdentity } from '../models/IUserIdentity';
import { useUtils } from '../utils/useUtils';
export const useAcsApi = () => {
const { GlobalState, setGlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { context, topic, useIdentity, acsConnectString, moderatorInfo } = GlobalState || ({} as IState);
const { instanceId } = context || {};
const { email } = context?.pageContext?.user || {};
const { displayName } = useIdentity || { displayName: context?.pageContext?.user.displayName };
const {checkIfSupportListExists, getThreadInformationFromSupportList, saveThreadInformationToSupportList, createChatSupportList } = useUtils();
const errorHandler = React.useCallback(
(error: Error, origem: string) => {
setGlobalState({
type: EActionTypes.SET_ERROR_INFO,
payload: { hasError: true, error } as IErrorInfo,
});
console.log(`Error in ${origem}: ${error.message}`);
},
[setGlobalState]
);
const getIdentityClient = React.useCallback(async () => {
const identityClient = new CommunicationIdentityClient(acsConnectString);
return identityClient;
}, [acsConnectString]);
const getToken = React.useCallback(
async (
identityClient: CommunicationIdentityClient,
userId: CommunicationUserIdentifier
): Promise<CommunicationAccessToken> => {
return await identityClient?.getToken(userId, ["chat"]);
},
[]
);
// get user identity (usersId and Token)
const getUserIdentity = React.useCallback(async (): Promise<IUserIdentity> => {
if (!displayName || !acsConnectString || !context) return undefined;
const identityClient = await getIdentityClient();
const identityResponse = await identityClient.createUser();
const tokenResponse = await getToken(identityClient, identityResponse);
const { token } = tokenResponse;
const credential = new AzureCommunicationTokenCredential({
tokenRefresher: async () => (await getToken(identityClient, identityResponse)).token,
refreshProactively: true,
});
return {
userId: identityResponse.communicationUserId,
accessToken: token,
displayName: displayName,
userCredential: credential,
email: email,
};
}, [acsConnectString, context, displayName, email, getIdentityClient, getToken]);
const getCredentials = React.useCallback(async (token: string): Promise<AzureCommunicationTokenCredential> => {
try {
if (!token) return undefined;
return new AzureCommunicationTokenCredential(token);
} catch {
console.error("Failed to construct token credential");
throw new Error("Failed to construct token credential");
}
}, []);
const createChatThread = React.useCallback(async (): Promise<IChatModeratorInfo> => {
if (!acsConnectString || !context) return undefined;
const configurationListExists = await checkIfSupportListExists();
if (!configurationListExists) {
await createChatSupportList();
}
let chatModeratorInfo: IChatModeratorInfo = await getThreadInformationFromSupportList(instanceId);
if (chatModeratorInfo) {
return chatModeratorInfo;
}
const userIdentity = await getUserIdentity();
if (!userIdentity) return undefined;
try {
const { userId, accessToken, userCredential } = userIdentity || ({} as IUserIdentity);
const client = new ChatClient(ENDPOINT_URL, userCredential);
const { chatThread } = await client.createChatThread(
{
topic: topic ?? "Room Chat",
},
{
participants: [
{
id: fromFlatCommunicationIdentifier(userId),
},
],
}
);
const threadId = chatThread?.id;
chatModeratorInfo = {
threadId: threadId,
moderatorAccessToken: accessToken,
moderatorUserId: userId,
email: email,
page: context?.pageContext?.web?.absoluteUrl,
} as IChatModeratorInfo;
await saveThreadInformationToSupportList(instanceId, chatModeratorInfo);
return chatModeratorInfo;
} catch (error) {
errorHandler(error, "createChatThread");
}
}, [
acsConnectString,
context,
email,
errorHandler,
getThreadInformationFromSupportList,
getUserIdentity,
instanceId,
saveThreadInformationToSupportList,
topic,
createChatSupportList,
checkIfSupportListExists
]);
const getCurrentThreadById = React.useCallback(
(threadId: string, moderatorAccessToken: string): ChatThreadClient => {
if (!threadId || !moderatorAccessToken) {
return undefined;
}
try {
const client = new ChatClient(ENDPOINT_URL, new AzureCommunicationTokenCredential(moderatorAccessToken));
const currentThread = client.getChatThreadClient(threadId);
return currentThread;
} catch (error) {
errorHandler(error, "getCurrentThreadById");
}
},
[errorHandler]
);
const addParticipantsToChatThreadClient = React.useCallback(
async (userId: string, displayName: string): Promise<void> => {
if (!moderatorInfo || !userId || !displayName) return;
try {
const { threadId, moderatorAccessToken } = moderatorInfo;
const chatThreadClient = getCurrentThreadById(threadId, moderatorAccessToken);
await chatThreadClient.addParticipants({
participants: [{ id: fromFlatCommunicationIdentifier(userId), displayName: displayName }],
});
} catch (error) {
errorHandler(error, "addParticipantsToChatThreadClient");
}
},
[errorHandler, getCurrentThreadById, moderatorInfo]
);
const createAdapter = React.useCallback(
async (userName: string, userId: string, credential, threadId: string): Promise<ChatAdapter> => {
if (!userName || !userId || !credential || !threadId) return undefined;
try {
const chatAdapterArgs = {
endpoint: ENDPOINT_URL,
userId: fromFlatCommunicationIdentifier(userId) as CommunicationUserIdentifier,
displayName: userName,
credential,
threadId: threadId,
};
// eslint-disable-next-line @typescript-eslint/typedef
const chatAdapter = await createAzureCommunicationChatAdapter(chatAdapterArgs);
return chatAdapter;
} catch (error) {
errorHandler(error, "createAdapter");
}
},
[errorHandler]
);
return {
errorHandler,
createChatThread,
getUserIdentity,
getCredentials,
createAdapter,
getCurrentThreadById,
addParticipantsToChatThreadClient,
};
};

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,7 @@
export interface IChatModeratorInfo {
threadId: string;
moderatorAccessToken : string
moderatorUserId: string;
email: string;
pageUrl?:string;
}

View File

@ -0,0 +1,16 @@
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
import { ChatAdapter } from '@azure/communication-react';
import { EProcessingStatus } from '../constants/EProcessingStatus';
export interface ICommunicationServiceData {
endpointUrl: string;
userId: string;
token: string;
displayName: string;
threadId: string;
chatAdapter: ChatAdapter;
credential: AzureCommunicationTokenCredential;
status: EProcessingStatus;
error: Error;
}

View File

@ -0,0 +1,3 @@
export interface IErrorInfo {
hasError: boolean, error:Error
}

View File

@ -0,0 +1,5 @@
import { IUserIdentity } from './IUserIdentity';
export interface IParticipant {
userInfo: IUserIdentity;
}

View File

@ -0,0 +1,11 @@
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
export interface IUserIdentity {
userId?:string;
accessToken?:string;
displayName:string;
avatar?:string;
threadId?:string;
userCredential?:AzureCommunicationTokenCredential;
email:string;
}

View File

@ -0,0 +1,43 @@
export const CAT: string = "🐱";
export const MOUSE: string = "🐭";
export const KOALA: string = "🐨";
export const OCTOPUS: string = "🐙";
export const MONKEY: string = "🐵";
export const FOX: string = "🦊";
export const GIRL: string = "👩";
export const BOY: string = "🧔";
export const MAN: string = "👨‍🦲";
export const WOMAN: string = "👩‍🦱";
export const getBackgroundColor = (avatar: string): { backgroundColor: string } => {
switch (avatar) {
case CAT:
return {
backgroundColor: "rgb(255, 250, 228)",
};
case MOUSE:
return {
backgroundColor: "rgb(232, 242, 249)",
};
case KOALA:
return {
backgroundColor: "rgb(237, 232, 230)",
};
case OCTOPUS:
return {
backgroundColor: "rgb(255, 240, 245)",
};
case MONKEY:
return {
backgroundColor: "rgb(255, 245, 222)",
};
case FOX:
return {
backgroundColor: "rgb(255, 231, 205)",
};
default:
return {
backgroundColor: "rgb(255, 250, 228)",
};
}
};

View File

@ -0,0 +1,15 @@
export enum LocalStorageKeys {
chatConfigurationList = "chat_configuration_list",
}
/**
*
*/
export const getFromLocalStorage = (key: string): string | undefined =>
window.localStorage.getItem(`${key}${LocalStorageKeys.chatConfigurationList}`);
/**
*
*/
export const saveToLocalStorage = (key: string,value:string): void =>
window.localStorage.setItem(`${key}${LocalStorageKeys.chatConfigurationList}`, value);

View File

@ -0,0 +1,36 @@
import '@pnp/sp/webs';
import '@pnp/sp/lists';
import '@pnp/sp/Fields';
import '@pnp/sp/items';
import '@pnp/sp/batching';
import '@pnp/sp/security/web';
import '@pnp/sp/security/list';
import '@pnp/sp/site-users/web';
import '@pnp/sp/sputilities';
import '@pnp/sp/site-groups/web';
import '@pnp/sp/sharing/web';
import { isEmpty } from '@microsoft/sp-lodash-subset';
import { WebPartContext } from '@microsoft/sp-webpart-base';
// import pnp and pnp logging system
import {
ISPFXContext,
spfi,
SPFI,
SPFx,
} from '@pnp/sp';
let _sp: SPFI = null;
export const getSP = (context?: WebPartContext): SPFI => {
try {
if ( !isEmpty(context) ) {
//You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
// The LogLevel set's at what level a message will be written to the console
_sp = spfi().using(SPFx((context as unknown) as ISPFXContext));
}
return _sp;
} catch (error) {
console.log(error);
}
};

View File

@ -0,0 +1,238 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @microsoft/spfx/no-async-await */
/* eslint-disable @typescript-eslint/typedef */
import '@pnp/sp/appcatalog';
import '@pnp/sp/webs';
import * as React from 'react';
import { SPFI } from '@pnp/sp';
import {
IList,
IListEnsureResult,
} from '@pnp/sp/lists';
import {
CHAT_KEY_ID,
SUPPORT_LIST,
} from '../constants';
import {
GlobalStateContext,
IGlobalStateContext,
IState,
} from '../globalStateProvider';
import { IChatModeratorInfo } from '../models/IChatModeratorInfo';
import { IParticipant } from '../models/IParticipant';
import { IUserIdentity } from '../models/IUserIdentity';
import {
getFromLocalStorage,
saveToLocalStorage,
} from './localStorage';
import { getSP } from './pnpjsConfig';
export const useUtils = () => {
const { GlobalState } = React.useContext<IGlobalStateContext>(GlobalStateContext);
const { context } = GlobalState || ({} as IState);
const sp: SPFI = React.useMemo(() => getSP(), []);
const addListPermissions = React.useCallback(async (list: IList): Promise<void> => {
await list.breakRoleInheritance(true);
if (!sp) return;
const visitorGroup = await sp.web.associatedVisitorGroup();
const defs = await sp.web.roleDefinitions();
await list.roleAssignments.add(visitorGroup.Id, defs.find((item) => item.Name === "Contribute").Id);
}, [sp]);
const addListFields = React.useCallback(async (list: IList): Promise<void> => {
const moderatorFieldAddResult = await list.fields.addMultilineText("moderatorInfo", {
NumberOfLines: 1000,
RichText: false,
RestrictedMode: false,
AppendOnly: false,
AllowHyperlink: false,
Group: "Chat Room Group",
});
await moderatorFieldAddResult.field.update({ Title: "Moderator Info" });
const participantsFieldAddResult = await list.fields.addMultilineText("Participants", {
NumberOfLines: 1000,
RichText: false,
RestrictedMode: false,
AppendOnly: false,
AllowHyperlink: false,
Group: "Chat Room Group",
});
await participantsFieldAddResult.field.update({ Title: "Participants" });
const totalParticipantsFieldAddResult = await list.fields.addText("total participants", {
Required: false,
Description: "total participants",
MaxLength: 255,
Group: "Chat Room Group",
});
await totalParticipantsFieldAddResult.field.update({ Title: "Total Participants" });
const pageUrlFieldAddResult = await list.fields.addText("Page", {
Required: false,
Description: "Page",
MaxLength: 255,
Group: "Chat Room Group",
});
await pageUrlFieldAddResult.field.update({ Title: "Page" });
await list.fields.getByInternalNameOrTitle("Title").update({ Title: "ThreadId" });
}, []);
const createChatSupportList = React.useCallback(
async (): Promise<IList> => {
if (!sp) return;
let ensureResult: IListEnsureResult;
try {
ensureResult = await sp.web.lists.ensure(SUPPORT_LIST, "Room Chat support List", 100, true);
const { list } = ensureResult || undefined;
const webId = context.pageContext.legacyPageContext.webId;
if (ensureResult.created) {
// if we've got the list
// we need to add the custom fields to the list
if (list) {
await addListFields(list);
// add a role assignment
await addListPermissions(list);
saveToLocalStorage(webId, 'RoomChat cfg list Exists' )
}
}
} catch (error) {
console.log(error);
}
return ensureResult?.list || undefined;
},
[addListFields, addListPermissions, context, sp]
);
const checkIfSupportListExists = React.useCallback(async (): Promise<boolean> => {
if (!context) return false;
const webId = context.pageContext.legacyPageContext.webId;
const storageValue = getFromLocalStorage(webId) || undefined;
return storageValue !== undefined && storageValue !== "";
}, [context]);
const getThreadInformationFromSupportList = React.useCallback(async (instanceId: string): Promise<
IChatModeratorInfo
> => {
if (!sp) return undefined;
const item = await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.top(1)
.filter(`Title eq '${instanceId}${CHAT_KEY_ID}'`)
.select("moderatorInfo")();
const moderatorInfo = item[0]?.moderatorInfo || undefined;
console.log("moderatorInfo)", moderatorInfo);
return moderatorInfo ? JSON.parse(moderatorInfo) : undefined;
}, [sp]);
const saveThreadInformationToSupportList = React.useCallback(
async (instanceId: string, chatInfo: IChatModeratorInfo): Promise<void> => {
if (!sp) return undefined;
await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.add({ Title: `${instanceId}${CHAT_KEY_ID}`, moderatorInfo: JSON.stringify(chatInfo) });
},
[sp]
);
const getChatParticipantsFromSupportList = React.useCallback(async (instanceId: string): Promise<IParticipant[]> => {
if (!sp) return undefined;
//const listExists: boolean = await checkIfSupportListExists();
const item = await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.top(1)
.filter(`Title eq '${instanceId}${CHAT_KEY_ID}'`)
.select("Participants")();
return item[0]?.Participants ? (JSON.parse(item[0]?.Participants) as IParticipant[]) : [];
}, [sp]);
const saveChatParticipantToSupportList = React.useCallback(
async (instanceId: string, userInfo: IUserIdentity): Promise<void> => {
if (!sp) return undefined;
const items = await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.top(1)
.filter(`Title eq '${instanceId}${CHAT_KEY_ID}'`)
.select("Participants, Id")();
if (items.length) {
const savedList: IParticipant[] = JSON.parse(items[0].Participants) ?? [];
savedList?.push({ userInfo: userInfo });
await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.getById(items[0].Id)
.update({
Participants: JSON.stringify(savedList),
});
}
},
[sp]
);
const getChatParticipantFromSupportList = React.useCallback(
async (instanceId: string, userId: string): Promise<IUserIdentity> =>
getChatParticipantsFromSupportList(instanceId).then(
(participants) =>
new Promise((resolve) => {
const userInfo = participants.filter((item) => item.userInfo.userId === userId)[0]?.userInfo ?? undefined;
return resolve(userInfo);
})
),
[getChatParticipantsFromSupportList]
);
const removeChatParticipantFromSupportList = React.useCallback(
async (instanceId: string, userId: string): Promise<void> => {
if (!sp) return undefined;
const savedList: IParticipant[] = (await getChatParticipantsFromSupportList(instanceId)) || [];
const newList: IParticipant[] = savedList.filter((item) => item.userInfo.userId !== userId) ?? [];
const items = await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.top(1)
.filter(`Title eq '${instanceId}${CHAT_KEY_ID}'`)
.select("Participants, Id")();
await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.getById(items[0].Id)
.update({
Participants: JSON.stringify(newList),
});
},
[getChatParticipantsFromSupportList, sp]
);
const deleteTreadInfoFromSupportList = React.useCallback(async (page: string): Promise<void> => {
if (!sp) return undefined;
const items = await sp.web.lists
.getByTitle(SUPPORT_LIST)
.items.filter(`Page eq '${page}'`)
.select("Participants, Id")();
if (items.length) {
await sp.web.lists.getByTitle(SUPPORT_LIST).items.getById(items[0].Id).delete();
}
}, [sp]);
return {
createChatSupportList,
deleteTreadInfoFromSupportList,
getChatParticipantFromSupportList,
saveChatParticipantToSupportList,
removeChatParticipantFromSupportList,
getChatParticipantsFromSupportList,
getThreadInformationFromSupportList,
saveThreadInformationToSupportList,
checkIfSupportListExists,
};
};

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "3a3d2b58-96a5-4b5c-a6cb-173435bc4fc1",
"alias": "RoomChatWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "SPFx Custom Apps" },
"title": { "default": "RoomChat" },
"description": { "default": "Room Chat" },
"officeFabricIconFontName": "ChatInviteFriend",
"properties": {
"topic": "Room Chat"
}
}]
}

View File

@ -0,0 +1,119 @@
/* eslint-disable @microsoft/spfx/no-async-await */
import * as React from 'react';
import * as ReactDom from 'react-dom';
import * as strings from 'RoomChatWebPartStrings';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IRoomChatProps } from '../../components/RoomChat/IRoomChatProps';
import { RoomChat } from '../../components/RoomChat/RoomChat';
import { getSP } from '../../utils/pnpjsConfig';
export interface IRoomChatWebChatProps {
topic: string;
acsConnectString:string;
}
export default class RoomChatWebChat extends BaseClientSideWebPart<IRoomChatWebChatProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
private _theme : IReadonlyTheme | undefined;
protected async onInit(): Promise<void> {
this._environmentMessage = this._getEnvironmentMessage();
console.log('legacy', this.context.pageContext.legacyPageContext)
getSP(this.context);
return super.onInit();
}
public render(): void {
const element: React.ReactElement<IRoomChatProps> = React.createElement(
RoomChat,
{
topic: this.properties.topic,
isDarkTheme: this._isDarkTheme,
theme: this._theme,
context: this.context,
acsConnectString: this.properties.acsConnectString
}
);
ReactDom.render(element, this.domElement);
}
private _getEnvironmentMessage(): string {
if (!!this.context.sdks.microsoftTeams) { // running in Teams
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
}
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment;
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
this.domElement.style.setProperty('--link', semanticColors.link);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
this._theme = currentTheme;
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('topic', {
label: strings.TopicFieldLabel,
value: this.properties.topic
}),
PropertyPaneTextField('acsConnectString', {
label: strings.ACSConnectStringFieldLabel,
onGetErrorMessage: (value: string) => {
if (value.length < 1) {
return strings.ascConnectringErrorMessage;
}
return '';
}
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,21 @@
define([], function() {
return {
ascConnectringErrorMessage: "Please enter a valid ACS Connect String",
BurttonLabelCancel: "Cancel",
ButtonLabelConfigure: "Configure",
ButtonLabelJoin: "Join",
ConfigureMessageLabel: "Please configure the Room Chat",
DialogSubTitleLabel: "Please enter your name to join the chat room.",
DialogTitleLabel: "Join",
EnterNameLabel: "Enter your name",
LeaveChatLabel: "Leave chat",
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"TopicFieldLabel": "Room Topic",
"ACSConnectStringFieldLabel": "ACS Connect String",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams"
}
});

View File

@ -0,0 +1,24 @@
declare interface IRoomChatWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TopicFieldLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
ACSConnectStringFieldLabel:string;
ascConnectringErrorMessage: string;
BurttonLabelCancel: string;
ButtonLabelConfigure: string;
ButtonLabelJoin: string;
ConfigureMessageLabel: string;
DialogSubTitleLabel: string;
DialogTitleLabel: string;
EnterNameLabel: string;
LeaveChatLabel: string;
}
declare module 'RoomChatWebPartStrings' {
const strings: IRoomChatWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.5/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}