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 ( -
-
- - {'Jitsi Meeting'} - - )} - > -
{ - this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover}); - }} - > -
-
- this.state.popoverTarget} - onHide={() => this.setState({showPopover: false})} - placement='bottom' - > - -
-
-
-
-
-
-
-
-
-
-
- -
- ); - } -} - -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 ( +