diff --git a/README.md b/README.md
index b654b10f..fc9761b5 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Inside the `/webapp` directory, you will find the JS and React files that make u
## TODO
There is still a lot of work to do to separate this from the Zoom plugin, such as:
-1. Convert names from zoom to jitsi
+1. ~~Convert names from zoom to jitsi~~
2. Integrate with the Jitsi server for meeting status
3. Clean up a lot of unnecessary code
+4. Add meeting topics back
\ No newline at end of file
diff --git a/plugin.json b/plugin.json
index 30a9e3b9..4d7660dc 100644
--- a/plugin.json
+++ b/plugin.json
@@ -2,12 +2,12 @@
"id": "jitsi",
"name": "Jitsi",
"description": "Jitsi audio and video conferencing plugin for Mattermost. Follow https://github.com/seansackowitz/mattermost-plugin-jitsi for notifications on updates.",
- "version": "0.2.0",
+ "version": "1.0.0",
"backend": {
"executable": "server/plugin.exe"
},
"webapp": {
- "bundle_path": "/static/jitsi_bundle.js"
+ "bundle_path": "webapp/jitsi_bundle.js"
},
"settings_schema": {
"settings": [
diff --git a/server/config.go b/server/config.go
deleted file mode 100644
index 34f7293e..00000000
--- a/server/config.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package main
-
-import (
- "fmt"
-)
-
-type Configuration struct {
- JitsiURL string
-}
-
-func (c *Configuration) IsValid() error {
- if len(c.JitsiURL) == 0 {
- return fmt.Errorf("JitsiURL is not configured.")
- }
-
- return nil
-}
diff --git a/server/main.go b/server/main.go
index cd8ff1aa..3eefb264 100644
--- a/server/main.go
+++ b/server/main.go
@@ -1,9 +1,7 @@
package main
-import (
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
+import "github.com/mattermost/mattermost-server/plugin"
func main() {
- rpcplugin.Main(&Plugin{})
+ plugin.ClientMain(&Plugin{})
}
diff --git a/server/plugin.go b/server/plugin.go
index 2e15b944..2669f32e 100644
--- a/server/plugin.go
+++ b/server/plugin.go
@@ -7,7 +7,6 @@ import (
"net/http"
"regexp"
"strings"
- "sync/atomic"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
@@ -18,42 +17,27 @@ const (
)
type Plugin struct {
- api plugin.API
- configuration atomic.Value
-}
+ plugin.MattermostPlugin
-func (p *Plugin) OnActivate(api plugin.API) error {
- p.api = api
- if err := p.OnConfigurationChange(); err != nil {
- return err
- }
+ JitsiURL string
+}
- config := p.config()
- if err := config.IsValid(); err != nil {
+func (p *Plugin) OnActivate() error {
+ if err := p.IsConfigurationValid(); err != nil {
return err
}
return nil
}
-func (p *Plugin) config() *Configuration {
- return p.configuration.Load().(*Configuration)
-}
-
-func (p *Plugin) OnConfigurationChange() error {
- var configuration Configuration
- err := p.api.LoadPluginConfiguration(&configuration)
- p.configuration.Store(&configuration)
- return err
-}
-
-func (p *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- config := p.config()
- if err := config.IsValid(); err != nil {
- http.Error(w, "This plugin is not configured.", http.StatusNotImplemented)
- return
+func (p *Plugin) IsConfigurationValid() error {
+ if len(p.JitsiURL) == 0 {
+ return fmt.Errorf("Jitsi URL is not configured.")
}
+ return nil
+}
+func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
switch path := r.URL.Path; path {
case "/api/v1/meetings":
p.handleStartMeeting(w, r)
@@ -93,12 +77,12 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
var user *model.User
var err *model.AppError
- user, err = p.api.GetUser(userId)
+ user, err = p.API.GetUser(userId)
if err != nil {
http.Error(w, err.Error(), err.StatusCode)
}
- if _, err := p.api.GetChannelMember(req.ChannelId, user.Id); err != nil {
+ if _, err := p.API.GetChannelMember(req.ChannelId, user.Id); err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
@@ -108,14 +92,14 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
if len(req.Topic) < 1 {
meetingID = generateRoomWithoutSeparator()
}
- jitsiURL := strings.TrimSpace(p.config().JitsiURL)
+ jitsiURL := strings.TrimSpace(p.JitsiURL)
meetingURL := jitsiURL + "/" + meetingID
post := &model.Post{
UserId: user.Id,
ChannelId: req.ChannelId,
Message: fmt.Sprintf("Meeting started at %s.", meetingURL),
- Type: "custom_zoom",
+ Type: "custom_jitsi",
Props: map[string]interface{}{
"meeting_id": meetingID,
"meeting_link": meetingURL,
@@ -128,11 +112,11 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
},
}
- if post, err := p.api.CreatePost(post); err != nil {
+ if post, err := p.API.CreatePost(post); err != nil {
http.Error(w, err.Error(), err.StatusCode)
return
} else {
- err = p.api.KeyValueStore().Set(fmt.Sprintf("%v%v", POST_MEETING_KEY, meetingID), []byte(post.Id))
+ err = p.API.KVSet(fmt.Sprintf("%v%v", POST_MEETING_KEY, meetingID), []byte(post.Id))
if err != nil {
http.Error(w, err.Error(), err.StatusCode)
return
diff --git a/webapp/actions/index.js b/webapp/actions/index.js
index 1e93bb32..467464f0 100644
--- a/webapp/actions/index.js
+++ b/webapp/actions/index.js
@@ -8,7 +8,7 @@ export function startMeeting(channelId, personal = false, topic = '', meetingId
await Client.startMeeting(channelId, personal, topic, meetingId);
} catch (error) {
const post = {
- id: 'zoomPlugin' + Date.now(),
+ id: 'jitsiPlugin' + Date.now(),
create_at: Date.now(),
update_at: 0,
edit_at: 0,
@@ -19,7 +19,7 @@ export function startMeeting(channelId, personal = false, topic = '', meetingId
root_id: '',
parent_id: '',
original_id: '',
- message: 'We could not verify your Mattermost account in Zoom. Please ensure that your Mattermost email address matches your Zoom email address.',
+ message: 'We could not start a meeting at this time.',
type: 'system_ephemeral',
props: {},
hashtags: '',
diff --git a/webapp/client/client.js b/webapp/client/client.js
index 047b645b..e7c89eae 100644
--- a/webapp/client/client.js
+++ b/webapp/client/client.js
@@ -5,7 +5,7 @@ export default class Client {
this.url = '/plugins/jitsi';
}
- startMeeting = async (channelId, personal = true, topic = '', meetingId = 0) => {
+ startMeeting = async (channelId, personal = false, topic = '', meetingId = 0) => {
return this.doPost(`${this.url}/api/v1/meetings`, {channel_id: channelId, personal, topic, meeting_id: meetingId});
}
diff --git a/webapp/components/channel_header_button/channel_header_button.jsx b/webapp/components/channel_header_button/channel_header_button.jsx
deleted file mode 100644
index 8ea457f8..00000000
--- a/webapp/components/channel_header_button/channel_header_button.jsx
+++ /dev/null
@@ -1,274 +0,0 @@
-const React = window.react;
-const {Overlay, OverlayTrigger, Popover, Tooltip} = window['react-bootstrap'];
-
-import ShareMeetingModal from '../share_meeting_modal';
-
-import {Svgs} from '../../constants';
-
-import PropTypes from 'prop-types';
-import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils';
-
-export default class ChannelHeaderButton extends React.PureComponent {
- static propTypes = {
- /*
- * The current channel ID
- */
- channelId: PropTypes.string.isRequired,
-
- /*
- * Logged in user's theme
- */
- theme: PropTypes.object.isRequired,
-
- actions: PropTypes.shape({
-
- /*
- * Action to start a meeting
- */
- startMeeting: PropTypes.func.isRequired
- }).isRequired
- }
-
- constructor(props) {
- super(props);
-
- this.state = {
- showPopover: false,
- rowStartHover: false,
- rowStartWithTopicHover: false,
- rowShareHover: false,
- showModal: false,
- shareModal: false
- };
- }
-
- rowStartShowHover = () => {
- this.setState({rowStartHover: true});
- }
-
- rowStartHideHover = () => {
- this.setState({rowStartHover: false});
- }
-
- rowStartWithTopicShowHover = () => {
- this.setState({rowStartWithTopicHover: true});
- }
-
- rowStartWithTopicHideHover = () => {
- this.setState({rowStartWithTopicHover: false});
- }
-
- rowShareShowHover = () => {
- this.setState({rowShareHover: true});
- }
-
- rowShareHideHover = () => {
- this.setState({rowShareHover: false});
- }
-
- resetHover = () => {
- this.hideHover();
- this.rowStartHideHover();
- this.rowStartWithTopicHideHover();
- this.rowShareHideHover();
- }
-
- showModal = () => {
- this.setState({showPopover: false, showModal: true, shareModal: false});
- this.resetHover();
- }
-
- showModalAsShare = () => {
- this.setState({showPopover: false, showModal: true, shareModal: true});
- this.resetHover();
- }
-
- hideModal = () => {
- this.setState({showModal: false});
- this.resetHover();
- }
-
- startMeeting = async () => {
- await this.props.actions.startMeeting(this.props.channelId, true, this.state.topic);
- this.setState({showPopover: false});
- this.resetHover();
- }
-
- render() {
- if (this.props.channelId === '') {
- return
;
- }
-
- const style = getStyle(this.props.theme);
-
- return (
-
-
-
-
- );
- }
-}
-
-const getStyle = makeStyleFromTheme((theme) => {
- return {
- iconStyle: {
- position: 'relative',
- top: '-1px'
- },
- popover: {
- marginLeft: '-100px',
- maxWidth: '285px',
- height: '155px',
- width: '285px',
- background: theme.centerChannelBg
- },
- popoverBody: {
- maxHeight: '305px',
- overflow: 'auto',
- position: 'relative',
- width: '283px',
- left: '-14px',
- top: '-9px'
- },
- popoverRow: {
- border: 'none',
- cursor: 'pointer',
- height: '50px',
- margin: '1px 0',
- overflow: 'hidden',
- padding: '6px 19px 0 10px'
- },
- popoverRowNoHover: {
- borderLeft: '3px solid',
- borderColor: theme.centerChannelBg,
- fontWeight: 'normal'
- },
- popoverRowHover: {
- borderLeft: '3px solid transparent',
- borderColor: theme.linkColor,
- background: changeOpacity(theme.linkColor, 0.08),
- fontWeight: 'bold'
- },
- popoverText: {
- fontWeight: 'inherit',
- fontSize: '14px',
- position: 'relative',
- top: '10px',
- left: '4px'
- },
- popoverIcon: {
- margin: '0',
- paddingLeft: '16px',
- position: 'relative',
- top: '14px',
- fontSize: '18px',
- fill: theme.buttonBg
- }
- };
-});
diff --git a/webapp/components/channel_header_button/index.js b/webapp/components/channel_header_button/index.js
deleted file mode 100644
index d14b41c0..00000000
--- a/webapp/components/channel_header_button/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const {connect} = window['react-redux'];
-const {bindActionCreators} = window.redux;
-
-import {startMeeting} from '../../actions';
-
-import ChannelHeaderButton from './channel_header_button.jsx';
-
-function mapStateToProps(state, ownProps) {
- let channelId = state.entities.channels.currentChannelId;
- const channel = state.entities.channels.channels[channelId] || {};
- const userId = state.entities.users.currentUserId;
- if (channel.name === `${userId}__${userId}`) {
- channelId = '';
- }
-
- return {
- channelId,
- ...ownProps
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators({
- startMeeting
- }, dispatch)
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(ChannelHeaderButton);
diff --git a/webapp/components/channel_header_button/popover.css b/webapp/components/channel_header_button/popover.css
deleted file mode 100644
index 187d0f65..00000000
--- a/webapp/components/channel_header_button/popover.css
+++ /dev/null
@@ -1,2 +0,0 @@
-.popover-content {
-}
diff --git a/webapp/components/icon.jsx b/webapp/components/icon.jsx
new file mode 100644
index 00000000..84d52f7f
--- /dev/null
+++ b/webapp/components/icon.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import {Svgs} from '../constants';
+import {makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
+
+export default class Icon extends React.PureComponent {
+ render() {
+ const style = getStyle();
+ return (
+
+ );
+ }
+}
+const getStyle = makeStyleFromTheme(() => {
+ return {
+ iconStyle: {
+ position: 'relative',
+ top: '-1px',
+ }
+ };
+});
\ No newline at end of file
diff --git a/webapp/components/mobile_channel_header_button/index.js b/webapp/components/mobile_channel_header_button/index.js
deleted file mode 100644
index d973f764..00000000
--- a/webapp/components/mobile_channel_header_button/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const {connect} = window['react-redux'];
-const {bindActionCreators} = window.redux;
-
-import {startMeeting} from '../../actions';
-
-import MobileChannelHeaderButton from './mobile_channel_header_button.jsx';
-
-function mapStateToProps(state, ownProps) {
- let channelId = state.entities.channels.currentChannelId;
- const channel = state.entities.channels.channels[channelId] || {};
- const userId = state.entities.users.currentUserId;
- if (channel.name === `${userId}__${userId}`) {
- channelId = '';
- }
-
- return {
- channelId,
- ...ownProps
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators({
- startMeeting
- }, dispatch)
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeaderButton);
diff --git a/webapp/components/mobile_channel_header_button/mobile_channel_header_button.jsx b/webapp/components/mobile_channel_header_button/mobile_channel_header_button.jsx
deleted file mode 100644
index 86c816ac..00000000
--- a/webapp/components/mobile_channel_header_button/mobile_channel_header_button.jsx
+++ /dev/null
@@ -1,144 +0,0 @@
-const React = window.react;
-
-import ShareMeetingModal from '../share_meeting_modal';
-
-import {Svgs} from '../../constants';
-
-import PropTypes from 'prop-types';
-import {makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
-
-const MIN_SCREEN_WIDTH = 480;
-
-export default class MobileChannelHeaderButton extends React.PureComponent {
- static propTypes = {
- /*
- * The current channel ID
- */
- channelId: PropTypes.string.isRequired,
-
- /*
- * Logged in user's theme
- */
- theme: PropTypes.object.isRequired,
-
- actions: PropTypes.shape({
-
- /*
- * Action to start a meeting
- */
- startMeeting: PropTypes.func.isRequired
- }).isRequired
- }
-
- constructor(props) {
- super(props);
-
- this.state = {
- showModal: false,
- shareModal: false
- };
- }
-
- showModal = () => {
- this.setState({showModal: true, shareModal: false});
- }
-
- showModalAsShare = () => {
- this.setState({showModal: true, shareModal: true});
- }
-
- hideModal = () => {
- this.setState({showModal: false});
- }
-
- startMeeting = async () => {
- await this.props.actions.startMeeting(this.props.channelId, true, this.state.topic);
- }
-
- render() {
- if (this.props.channelId === '' || window.innerWidth <= MIN_SCREEN_WIDTH) {
- return ;
- }
-
- const style = getStyle(this.props.theme);
-
- // TODO: convert CSS classes to style objects to remove reliance on webapp
- return (
-
- );
- }
-}
-
-const getStyle = makeStyleFromTheme((theme) => {
- return {
- icon: {
- position: 'relative',
- top: '-9px',
- left: '1px',
- fill: theme.buttonBg
- }
- };
-});
diff --git a/webapp/components/mobile_channel_header_button/popover.css b/webapp/components/mobile_channel_header_button/popover.css
deleted file mode 100644
index 187d0f65..00000000
--- a/webapp/components/mobile_channel_header_button/popover.css
+++ /dev/null
@@ -1,2 +0,0 @@
-.popover-content {
-}
diff --git a/webapp/components/post_type_zoom/index.js b/webapp/components/post_type_jitsi/index.js
similarity index 82%
rename from webapp/components/post_type_zoom/index.js
rename to webapp/components/post_type_jitsi/index.js
index e49e583e..6b4687fe 100644
--- a/webapp/components/post_type_zoom/index.js
+++ b/webapp/components/post_type_jitsi/index.js
@@ -1,10 +1,10 @@
-const {connect} = window['react-redux'];
-const {bindActionCreators} = window.redux;
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {displayUsernameForUser} from '../../utils/user_utils';
-import PostTypeZoom from './post_type_zoom.jsx';
+import PostTypeJitsi from './post_type_jitsi.jsx';
function mapStateToProps(state, ownProps) {
const post = ownProps.post || {};
@@ -24,4 +24,4 @@ function mapDispatchToProps(dispatch) {
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(PostTypeZoom);
+export default connect(mapStateToProps, mapDispatchToProps)(PostTypeJitsi);
diff --git a/webapp/components/post_type_zoom/post_type_zoom.jsx b/webapp/components/post_type_jitsi/post_type_jitsi.jsx
similarity index 98%
rename from webapp/components/post_type_zoom/post_type_zoom.jsx
rename to webapp/components/post_type_jitsi/post_type_jitsi.jsx
index 6d4418aa..72daadd8 100644
--- a/webapp/components/post_type_zoom/post_type_zoom.jsx
+++ b/webapp/components/post_type_jitsi/post_type_jitsi.jsx
@@ -1,4 +1,4 @@
-const React = window.react;
+import React from 'react';
import {Svgs} from '../../constants';
import {formatDate} from '../../utils/date_utils';
@@ -6,7 +6,7 @@ import {formatDate} from '../../utils/date_utils';
import PropTypes from 'prop-types';
import {makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
-export default class PostTypeZoom extends React.PureComponent {
+export default class PostTypeJitsi extends React.PureComponent {
static propTypes = {
/*
diff --git a/webapp/components/share_meeting_modal/index.js b/webapp/components/share_meeting_modal/index.js
deleted file mode 100644
index 6f15c183..00000000
--- a/webapp/components/share_meeting_modal/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const {connect} = window['react-redux'];
-const {bindActionCreators} = window.redux;
-
-import {startMeeting} from '../../actions';
-
-import ShareMeetingModal from './share_meeting_modal.jsx';
-
-function mapStateToProps(state, ownProps) {
- return ownProps;
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators({
- startMeeting
- }, dispatch)
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(ShareMeetingModal);
diff --git a/webapp/components/share_meeting_modal/share_meeting_modal.jsx b/webapp/components/share_meeting_modal/share_meeting_modal.jsx
deleted file mode 100644
index 1c43e740..00000000
--- a/webapp/components/share_meeting_modal/share_meeting_modal.jsx
+++ /dev/null
@@ -1,215 +0,0 @@
-const React = window.react;
-const {Modal} = window['react-bootstrap'];
-
-import PropTypes from 'prop-types';
-import {makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
-
-const ZOOM_MEETING_ID_LENGTH = [9, 10, 11];
-
-export default class ShareMeetingModal extends React.PureComponent {
- static propTypes = {
- /*
- * Set to true to show modal
- */
- show: PropTypes.bool.isRequired,
-
- /*
- * Set to true to show share options
- */
- share: PropTypes.bool.isRequired,
-
- /*
- * Current channel ID
- */
- channelId: PropTypes.string.isRequired,
-
- /*
- * Logged in user's theme
- */
- theme: PropTypes.object.isRequired,
-
- /*
- * Function to hide the modal
- */
- hide: PropTypes.func.isRequired,
-
- actions: PropTypes.shape({
-
- /*
- * Action to start a meeting
- */
- startMeeting: PropTypes.func.isRequired
- }).isRequired
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.show && !this.props.show) {
- this.setState({
- topic: '',
- meetingId: '',
- meetingIdError: null
- });
- }
- }
-
- constructor(props) {
- super(props);
-
- this.state = {
- topic: '',
- meetingId: '',
- meetingIdError: null
- };
- }
-
- onTopicChange = (e) => {
- this.setState({topic: e.target.value});
- }
-
- onMeetingIdChange = (e) => {
- this.setState({meetingId: e.target.value});
- }
-
- startMeeting = async () => {
- const meetingId = this.state.meetingId.trim().replace(/-/g, '');
- if (this.props.share && !ZOOM_MEETING_ID_LENGTH.includes(meetingId.length)) {
- this.setState({meetingIdError: 'Meeting ID must be a 9, 10 or 11-digit number'});
- return;
- }
-
- this.setState({meetingIdError: null});
-
- await this.props.actions.startMeeting(this.props.channelId, !this.props.share, this.state.topic, parseInt(meetingId, 10));
- this.props.hide();
- }
-
- onHide = () => {
- this.setState({
- topic: '',
- meetingId: '',
- meetingIdError: null
- });
-
- this.props.hide();
- }
-
- render() {
- const style = getStyle(this.props.theme);
-
- let title = 'Start Jitsi Meeting';
- let button = 'Start Meeting';
- let meetingIdInput;
- if (this.props.share) {
- title = 'Share Jitsi Meeting';
- button = 'Share Meeting';
-
- let error;
- if (this.state.meetingIdError) {
- error = (
-
- );
- }
-
- meetingIdInput = (
-
-
-
-
- {error}
-
-
- );
- }
-
- return (
-
-
-
- );
- }
-}
-
-const getStyle = makeStyleFromTheme((theme) => {
- return {
- title: {
- margin: '5px 0 0 0',
- fontSize: '17px'
- },
- label: {
- top: '6px'
- },
- error: {
- color: '#811519',
- marginTop: '10px',
- fontFamily: 'Open Sans',
- fontWeight: 'normal',
- fontSize: '14px',
- lineHeight: '19px'
- },
- meetingId: {
- marginTop: '55px'
- }
- };
-});
diff --git a/webapp/index.js b/webapp/index.js
index 247d1c17..1b5ca799 100644
--- a/webapp/index.js
+++ b/webapp/index.js
@@ -1,14 +1,23 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ChannelHeaderButton from './components/channel_header_button';
-import MobileChannelHeaderButton from './components/mobile_channel_header_button';
-import PostTypeZoom from './components/post_type_zoom';
+import React from 'react';
+
+import Icon from './components/icon.jsx';
+import PostTypeJitsi from './components/post_type_jitsi';
+import {startMeeting} from './actions'
class PluginClass {
- initialize(registerComponents, store) {
- registerComponents({ChannelHeaderButton, MobileChannelHeaderButton}, {custom_zoom: PostTypeZoom});
+ initialize(registry, store) {
+ registry.registerChannelHeaderButtonAction(
+ ,
+ (channel) => {
+ startMeeting(channel.id)(store.dispatch, store.getState);
+ },
+ 'Start Jitsi Meeting'
+ );
+ registry.registerPostTypeComponent('custom_jitsi', PostTypeJitsi)
}
}
-global.window.plugins['jitsi'] = new PluginClass();
+global.window.registerPlugin('jitsi', new PluginClass());
diff --git a/webapp/package.json b/webapp/package.json
index d8661479..7c96129d 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -5,6 +5,9 @@
"dependencies": {
"mattermost-redux": "1.0.1",
"prop-types": "15.5.10",
+ "react": "^16.4.1",
+ "react-redux": "^5.0.7",
+ "redux": "^4.0.0",
"superagent": "3.5.2"
},
"devDependencies": {
diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js
index 505301f0..7b902bd7 100644
--- a/webapp/webpack.config.js
+++ b/webapp/webpack.config.js
@@ -30,5 +30,10 @@ module.exports = {
]
}
]
+ },
+ externals: {
+ react: 'React',
+ redux: 'Redux',
+ 'react-redux': 'ReactRedux',
}
};