Skip to content

Commit

Permalink
feat(Meeting): implement one
Browse files Browse the repository at this point in the history
  • Loading branch information
akoushke committed Jan 23, 2020
1 parent f0ec657 commit 338b85d
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 1 deletion.
38 changes: 38 additions & 0 deletions src/components/WebexMeeting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Webex Meeting Component

Webex Meeting component displays the complete, default Webex meeting experience.

<p align="center">
<img src="./WebexMeeting.gif" alt="Default Webex Meeting" />
</p>

## Preview

To see all the different possible states of the Webex Meeting component, you can run our Storybook:

```shell
npm start
```

## Embed

1. Create a component adapter from which the data will be retrieved (See [adapters](../../adapters)). For instance:

```js
const jsonAdapter = new WebexJSONAdapter(jsonData);
```

2. Create a component instance by passing the meeting ID as a string and an optional function that returns an array
of control names for the meeting. The default control names are set to `['mute-audio', 'mute-video', 'join-meeting]` if the meeting is inactive and `['mute-audio', 'mute-video', 'leave-meeting']` otherwise. Ensure that the control names match with the adapter implementation of the controls. You then need to
enclose it within [a data provider](../WebexDataProvider/WebexDataProvider.js)
that takes the [component data adapter](../../adapters/WebexJSONAdapter.js) that we created previously

```js
const controls = (isActive) => isActive ? ['join-meeting'] : ['leave-meeting'];

<WebexDataProvider adapter={jsonAdapter}>
<WebexMeeting meetingDestination="meetingDestination" controls?={controls}/>
</WebexDataProvider>
```
The component knows how to manage its data. If anything changes in the data source that the adapter manages, the component will also update on its own.
15 changes: 15 additions & 0 deletions src/components/WebexMeeting/WebexMeeting.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.meeting {
display: flex;
position: relative;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}

.meeting .controls {
display: flex;
position: absolute;
align-self: flex-end;
bottom: 3rem;
}
Binary file added src/components/WebexMeeting/WebexMeeting.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions src/components/WebexMeeting/WebexMeeting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Spinner} from '@momentum-ui/react';

import {useMeeting} from '../hooks';

import {WebexInMeeting, WebexInterstitialMeeting, WebexMeetingControl, WebexMeetingControls} from '..';

import './WebexMeeting.css';

/**
* Webex Meeting component displays the default Webex meeting experience.
*
* @param {object} props
* @returns {object} JSX of the component
*/
export default function WebexMeeting({meetingDestination, controls}) {
const {ID} = useMeeting(null, meetingDestination);
// Subscribe to meeting updates after meeting creation
const {remoteVideo} = useMeeting(ID);
const isActive = remoteVideo !== null;
const meetingControls = controls(isActive).map((key) => <WebexMeetingControl key={key} type={key} />);

return (
<div className="meeting">
{ID ? (
<Fragment>
{isActive ? <WebexInMeeting meetingID={ID} /> : <WebexInterstitialMeeting meetingID={ID} />}
<WebexMeetingControls meetingID={ID}>{meetingControls}</WebexMeetingControls>
</Fragment>
) : (
<Spinner />
)}
</div>
);
}

WebexMeeting.propTypes = {
meetingDestination: PropTypes.string.isRequired,
controls: PropTypes.func,
};

WebexMeeting.defaultProps = {
/**
* A function that returns an array of control names for the meeting.
* Control name must match with the adapter implementation of the control.
*
* @param {boolean} isActive Whether or not the meeting is active
*/
// eslint-disable-next-line no-confusing-arrow
controls: (isActive) =>
isActive ? ['mute-audio', 'mute-video', 'leave-meeting'] : ['mute-audio', 'mute-video', 'join-meeting'],
};
32 changes: 32 additions & 0 deletions src/components/WebexMeeting/WebexMeeting.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import {storiesOf} from '@storybook/react';

import jsonData from '../../data';
import {WebexJSONAdapter} from '../../adapters';

import {WebexMeeting, WebexDataProvider} from '..';

// Setup for the stories
const stories = storiesOf('Webex Meeting', module);
const webexAdapter = new WebexJSONAdapter(jsonData);
const wrapperStyle = {height: '500px', width: '800px', border: '1px solid black'};

stories.add('in session', () => (
<div style={wrapperStyle}>
<WebexDataProvider adapter={webexAdapter}>
<WebexMeeting meetingDestination="localMedia" />
</WebexDataProvider>
</div>
));

stories.add('in session with optional controls', () => {
const controls = (isActive) => (isActive ? ['leave-meeting'] : ['join-meeting']);

return (
<div style={wrapperStyle}>
<WebexDataProvider adapter={webexAdapter}>
<WebexMeeting meetingDestination="localMedia" controls={controls} />
</WebexDataProvider>
</div>
);
});
35 changes: 35 additions & 0 deletions src/components/WebexMeeting/WebexMeeting.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

import WebexMeeting from './WebexMeeting';

jest.mock('../hooks/useMeeting');

describe('Webex Meeting component', () => {
describe('snapshot', () => {
let controls;

beforeAll(() => {
controls = (isActive) => (isActive ? ['leave-meeting'] : ['join-meeting']);
});

test('matches snapshot of loading while meeting is created', () => {
expect(shallow(<WebexMeeting meetingDestination="" />)).toMatchSnapshot();
});

test('matches snapshot of a user waiting to join a meeting', () => {
expect(shallow(<WebexMeeting meetingDestination="localMedia" />)).toMatchSnapshot();
});

test('matches snapshot of a user that has joined a meeting', () => {
expect(shallow(<WebexMeeting meetingDestination="remoteMedia" />)).toMatchSnapshot();
});

test('matches snapshot of a user waiting to join a meeting with optional controls', () => {
expect(shallow(<WebexMeeting meetingDestination="localMedia" controls={controls} />)).toMatchSnapshot();
});

test('matches snapshot of a user that has joined a meeting with optional controls', () => {
expect(shallow(<WebexMeeting meetingDestination="remoteMedia" controls={controls} />)).toMatchSnapshot();
});
});
});
104 changes: 104 additions & 0 deletions src/components/WebexMeeting/__snapshots__/WebexMeeting.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Webex Meeting component snapshot matches snapshot of a user that has joined a meeting 1`] = `
<div
className="meeting"
>
<WebexInMeeting
meetingID="remoteMedia"
/>
<WebexMeetingControls
meetingID="remoteMedia"
>
<WebexMeetingControl
key="mute-audio"
type="mute-audio"
/>
<WebexMeetingControl
key="mute-video"
type="mute-video"
/>
<WebexMeetingControl
key="leave-meeting"
type="leave-meeting"
/>
</WebexMeetingControls>
</div>
`;

exports[`Webex Meeting component snapshot matches snapshot of a user that has joined a meeting with optional controls 1`] = `
<div
className="meeting"
>
<WebexInMeeting
meetingID="remoteMedia"
/>
<WebexMeetingControls
meetingID="remoteMedia"
>
<WebexMeetingControl
key="leave-meeting"
type="leave-meeting"
/>
</WebexMeetingControls>
</div>
`;

exports[`Webex Meeting component snapshot matches snapshot of a user waiting to join a meeting 1`] = `
<div
className="meeting"
>
<WebexInterstitialMeeting
meetingID="localMedia"
/>
<WebexMeetingControls
meetingID="localMedia"
>
<WebexMeetingControl
key="mute-audio"
type="mute-audio"
/>
<WebexMeetingControl
key="mute-video"
type="mute-video"
/>
<WebexMeetingControl
key="join-meeting"
type="join-meeting"
/>
</WebexMeetingControls>
</div>
`;

exports[`Webex Meeting component snapshot matches snapshot of a user waiting to join a meeting with optional controls 1`] = `
<div
className="meeting"
>
<WebexInterstitialMeeting
meetingID="localMedia"
/>
<WebexMeetingControls
meetingID="localMedia"
>
<WebexMeetingControl
key="join-meeting"
type="join-meeting"
/>
</WebexMeetingControls>
</div>
`;

exports[`Webex Meeting component snapshot matches snapshot of loading while meeting is created 1`] = `
<div
className="meeting"
>
<Spinner
className=""
color="black"
percentage={null}
showCheck={false}
showPercentage={false}
size={36}
/>
</div>
`;
2 changes: 1 addition & 1 deletion src/components/hooks/__mocks__/useMeeting.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function useMeeting(meetingID, meetingDestination) {
if (meetingID in datasource.meetingsAdapter) {
result = datasource.meetingsAdapter[meetingID];
} else if (meetingDestination) {
result = datasource.meetingsAdapter.localMedia;
result = datasource.meetingsAdapter[meetingDestination];
}

return result;
Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {default as WebexDataProvider, AdapterContext} from './WebexDataProvider/
export {default as WebexInMeeting} from './WebexInMeeting/WebexInMeeting';
export {default as WebexInterstitialMeeting} from './WebexInterstitialMeeting/WebexInterstitialMeeting';
export {default as WebexLocalMedia} from './WebexLocalMedia/WebexLocalMedia';
export {default as WebexMeeting} from './WebexMeeting/WebexMeeting';
export {default as WebexMeetingControl} from './WebexMeetingControl/WebexMeetingControl';
export {default as WebexMeetingControls, MeetingContext} from './WebexMeetingControl/WebexMeetingControls';
export {default as WebexMeetingInfo} from './WebexMeetingInfo/WebexMeetingInfo';
Expand Down

0 comments on commit 338b85d

Please sign in to comment.