diff options
Diffstat (limited to 'web/react')
23 files changed, 821 insertions, 257 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index f2fb8ac78..f770d166c 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -40,9 +40,13 @@ export default class AdminController extends React.Component { config: AdminStore.getConfig(), teams: AdminStore.getAllTeams(), selectedTeams, - selected: 'service_settings', - selectedTeam: null + selected: props.tab || 'service_settings', + selectedTeam: props.teamId || null }; + + if (!props.tab) { + history.replaceState(null, null, `/admin_console/${this.state.selected}`); + } } componentDidMount() { @@ -142,7 +146,9 @@ export default class AdminController extends React.Component { } else if (this.state.selected === 'service_settings') { tab = <ServiceSettingsTab config={this.state.config} />; } else if (this.state.selected === 'team_users') { - tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; + if (this.state.teams) { + tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; + } } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 4c2a473b6..b0e01ff17 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -24,6 +24,7 @@ export default class AdminSidebar extends React.Component { handleClick(name, teamId, e) { e.preventDefault(); this.props.selectTab(name, teamId); + history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`); } isSelected(name, teamId) { @@ -53,6 +54,9 @@ export default class AdminSidebar extends React.Component { } componentDidMount() { + if ($(window).width() > 768) { + $('.nav-pills__container').perfectScrollbar(); + } } showTeamSelect(e) { diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 4105ba6da..53c89a942 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -36,6 +36,7 @@ export default class ServiceSettings extends React.Component { config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim(); config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim(); config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked; + config.ServiceSettings.EnableOutgoingWebhooks = React.findDOMNode(this.refs.EnableOutgoingWebhooks).checked; config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked; config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked; config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; @@ -207,7 +208,40 @@ export default class ServiceSettings extends React.Component { </div> </div> - <div className='form-group'> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableOutgoingWebhooks' + > + {'Enable Outgoing Webhooks: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableOutgoingWebhooks' + value='true' + ref='EnableOutgoingWebhooks' + defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableOutgoingWebhooks' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, outgoing webhooks will be allowed.'}</p> + </div> + </div> + + <div className='form-group'> <label className='control-label col-sm-4' htmlFor='EnablePostUsernameOverride' diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 7582de6c4..1b709336f 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -4,7 +4,6 @@ const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const PostStore = require('../stores/post_store.jsx'); -const SocketStore = require('../stores/socket_store.jsx'); const NavbarSearchBox = require('./search_bar.jsx'); const AsyncClient = require('../utils/async_client.jsx'); const Client = require('../utils/client.jsx'); @@ -25,7 +24,6 @@ export default class ChannelHeader extends React.Component { super(props); this.onListenerChange = this.onListenerChange.bind(this); - this.onSocketChange = this.onSocketChange.bind(this); this.handleLeave = this.handleLeave.bind(this); this.searchMentions = this.searchMentions.bind(this); @@ -45,7 +43,6 @@ export default class ChannelHeader extends React.Component { ChannelStore.addExtraInfoChangeListener(this.onListenerChange); PostStore.addSearchChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); - SocketStore.addChangeListener(this.onSocketChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); @@ -60,16 +57,9 @@ export default class ChannelHeader extends React.Component { } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } - onSocketChange(msg) { - if (msg.action === 'new_user' || - msg.action === 'user_added' || - (msg.action === 'user_removed' && msg.user_id !== UserStore.getCurrentId())) { - AsyncClient.getChannelExtraInfo(true); - } - } handleLeave() { Client.leaveChannel(this.state.channel.id, - function handleLeaveSuccess() { + () => { AppDispatcher.handleViewAction({ type: ActionTypes.LEAVE_CHANNEL, id: this.state.channel.id @@ -77,8 +67,8 @@ export default class ChannelHeader extends React.Component { const townsquare = ChannelStore.getByName('town-square'); Utils.switchChannel(townsquare); - }.bind(this), - function handleLeaveError(err) { + }, + (err) => { AsyncClient.dispatchError(err, 'handleLeave'); } ); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 2df3dc40f..12d1af6ff 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -32,6 +32,7 @@ export default class CreateComment extends React.Component { this.removePreview = this.removePreview.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.getFileCount = this.getFileCount.bind(this); + this.handleResize = this.handleResize.bind(this); PostStore.clearCommentDraftUploads(); @@ -40,13 +41,23 @@ export default class CreateComment extends React.Component { messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, - submitting: false + submitting: false, + windowWidth: Utils.windowWidth() }; } + componentDidMount() { + window.addEventListener('resize', this.handleResize); + } + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + handleResize() { + this.setState({windowWidth: Utils.windowWidth()}); + } componentDidUpdate(prevProps, prevState) { if (prevState.uploadsInProgress < this.state.uploadsInProgress) { $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('.post-right__scroll').perfectScrollbar('update'); } } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 2581bdcca..035899592 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -37,6 +37,7 @@ export default class CreatePost extends React.Component { this.onChange = this.onChange.bind(this); this.getFileCount = this.getFileCount.bind(this); this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleResize = this.handleResize.bind(this); PostStore.clearDraftUploads(); @@ -48,9 +49,17 @@ export default class CreatePost extends React.Component { uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, submitting: false, - initialText: draft.messageText + initialText: draft.messageText, + windowWidth: Utils.windowWidth(), + windowHeigth: Utils.windowHeight() }; } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } componentDidUpdate(prevProps, prevState) { if (prevState.previews.length !== this.state.previews.length) { this.resizePostHolder(); @@ -61,6 +70,11 @@ export default class CreatePost extends React.Component { this.resizePostHolder(); return; } + + if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) { + this.resizePostHolder(); + return; + } } getCurrentDraft() { const draft = PostStore.getCurrentDraft(); @@ -194,10 +208,9 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = $(window).height() - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; + const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; $('.post-list-holder-by-time').css('height', `${height}px`); - $(window).trigger('resize'); - if ($(window).width() > 960) { + if (this.state.windowWidth > 960) { $('#post_textbox').focus(); } } @@ -274,9 +287,11 @@ export default class CreatePost extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onChange); this.resizePostHolder(); + window.addEventListener('resize', this.handleResize); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); + window.removeEventListener('resize', this.handleResize); } onChange() { const channelId = ChannelStore.getCurrentId(); diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index a20c5cad5..a0084ad30 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -83,7 +83,7 @@ export default class MoreChannels extends React.Component { moreChannels = <LoadingScreen />; } else if (channels.length) { moreChannels = ( - <table className='more-channel-table table'> + <table className='more-table table'> <tbody> {channels.map(function cMap(channel, index) { var joinButton; @@ -108,8 +108,8 @@ export default class MoreChannels extends React.Component { return ( <tr key={channel.id}> <td> - <p className='more-channel-name'>{channel.display_name}</p> - <p className='more-channel-description'>{channel.description}</p> + <p className='more-name'>{channel.display_name}</p> + <p className='more-description'>{channel.description}</p> </td> <td className='td--action'> {joinButton} diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 08b64de8b..d5b44d86b 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -142,7 +142,6 @@ export default class MoreDirectChannels extends React.Component { details.push( <span key={`${user.nickname}__nickname`} - className='nickname' > {separator + user.nickname} </span> @@ -170,31 +169,40 @@ export default class MoreDirectChannels extends React.Component { } return ( - <li - key={user.id} - className='direct-channel' - > - <div className='col-xs-1 image-div'> + <tr> + <td + key={user.id} + className='direct-channel' + > <img - className='profile-image' + className='profile-img pull-left' + width='38' + height='38' src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} /> - </div> - <div className='col-xs-9'> - <div className='username'> + <div className='more-name'> {user.username} </div> - <div> + <div className='more-description'> {details} </div> - </div> - <div className='col-xs-2 btn-div'> + </td> + <td className='td--action lg'> {joinButton} - </div> - </li> + </td> + </tr> ); } + componentDidUpdate(prevProps) { + if (!prevProps.show && this.props.show) { + $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); + } + } + } + render() { if (!this.props.show) { return null; @@ -213,7 +221,7 @@ export default class MoreDirectChannels extends React.Component { const userEntries = users.map(this.createRowForUser); if (userEntries.length === 0) { - userEntries.push(<li key='no-users-found'>{'No users found :('}</li>); + userEntries.push(<tr key='no-users-found'><td>{'No users found :('}</td></tr>); } let memberString = 'Member'; @@ -232,26 +240,35 @@ export default class MoreDirectChannels extends React.Component { <Modal className='modal-direct-channels' show={this.props.show} - bsSize='large' onHide={this.handleHide} > <Modal.Header closeButton={true}> - <Modal.Title>{'More Direct Messages'}</Modal.Title> + <Modal.Title>{'Direct Messages'}</Modal.Title> </Modal.Header> <Modal.Body> - <div> - <input - ref='filter' - className='form-control filter-textbox' - placeholder='Search members' - onInput={this.handleFilterChange} - style={{width: '200px', display: 'inline'}} - /> - <span className='member-count pull-right'>{count}</span> + <div className='row filter-row'> + <div className='col-sm-6'> + <input + ref='filter' + className='form-control filter-textbox' + placeholder='Search members' + onInput={this.handleFilterChange} + /> + </div> + <div className='col-sm-6'> + <span className='member-count'>{count}</span> + </div> + </div> + <div + ref='userList' + className='user-list' + > + <table className='more-table table'> + <tbody> + {userEntries} + </tbody> + </table> </div> - <ul className='user-list'> - {userEntries} - </ul> </Modal.Body> <Modal.Footer> <button diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index 569942390..1bd23c55c 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -1,8 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var SocketStore = require('../stores/socket_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); + +const Constants = require('../utils/constants.jsx'); +const SocketEvents = Constants.SocketEvents; export default class MsgTyping extends React.Component { constructor(props) { @@ -33,9 +36,9 @@ export default class MsgTyping extends React.Component { } onChange(msg) { - if (msg.action === 'typing' && - this.props.channelId === msg.channel_id && - this.props.parentId === msg.props.parent_id) { + if (msg.action === SocketEvents.TYPING && + this.props.channelId === msg.channel_id && + this.props.parentId === msg.props.parent_id) { this.lastTime = new Date().getTime(); var username = 'Someone'; @@ -52,7 +55,7 @@ export default class MsgTyping extends React.Component { } }.bind(this), 3000); } - } else if (msg.action === 'posted' && msg.channel_id === this.props.channelId) { + } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { this.setState({text: ''}); } } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 29728d368..4402745e1 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -1,20 +1,24 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var PostStore = require('../stores/post_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var PreferenceStore = require('../stores/preference_store.jsx'); -var UserProfile = require('./user_profile.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Post = require('./post.jsx'); -var LoadingScreen = require('./loading_screen.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var utils = require('../utils/utils.jsx'); -var Client = require('../utils/client.jsx'); -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const Post = require('./post.jsx'); +const UserProfile = require('./user_profile.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); + +const PostStore = require('../stores/post_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); + +const utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const SocketEvents = Constants.SocketEvents; + +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); export default class PostList extends React.Component { constructor(props) { @@ -58,7 +62,7 @@ export default class PostList extends React.Component { } } - postList.order.sort(function postSort(a, b) { + postList.order.sort((a, b) => { if (postList.posts[a].create_at > postList.posts[b].create_at) { return -1; } @@ -82,7 +86,7 @@ export default class PostList extends React.Component { } return { - postList: postList + postList }; } componentDidMount() { @@ -263,14 +267,14 @@ export default class PostList extends React.Component { Client.getPosts( id, PostStore.getLatestUpdate(id), - function success() { + () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); - }.bind(this), - function fail() { + }, + () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); - }.bind(this) + } ); } onChange() { @@ -281,28 +285,16 @@ export default class PostList extends React.Component { } } onSocketChange(msg) { - var post; - if (msg.action === 'posted' || msg.action === 'post_edited') { - post = JSON.parse(msg.props.post); - PostStore.storePost(post); - } else if (msg.action === 'post_deleted') { + if (msg.action === SocketEvents.POST_DELETED) { var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; var activeRootPostId = ''; if (activeRoot && activeRoot.id.length > 0) { activeRootPostId = activeRoot.id; } - post = JSON.parse(msg.props.post); - - PostStore.storeUnseenDeletedPost(post); - PostStore.removePost(post, true); - PostStore.emitChange(); - if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); } - } else if (msg.action === 'new_user') { - AsyncClient.getProfiles(); } } onTimeChange() { @@ -352,7 +344,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>Set a description + <i className='fa fa-pencil'></i>{'Set a description'} </a> </div> ); diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index 467d74681..bcdec2870 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -4,7 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var PreferenceStore = require('../stores/preference_store.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SearchBox = require('./search_bar.jsx'); var CreateComment = require('./create_comment.jsx'); var RhsHeaderPost = require('./rhs_header_post.jsx'); @@ -20,8 +20,12 @@ export default class RhsThread extends React.Component { this.onChange = this.onChange.bind(this); this.onChangeAll = this.onChangeAll.bind(this); this.forceUpdateInfo = this.forceUpdateInfo.bind(this); + this.handleResize = this.handleResize.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.windowWidth = Utils.windowWidth(); + state.windowHeight = Utils.windowHeight(); + this.state = state; } getStateFromStores() { var postList = PostStore.getSelectedPost(); @@ -47,9 +51,7 @@ export default class RhsThread extends React.Component { PostStore.addChangeListener(this.onChangeAll); PreferenceStore.addChangeListener(this.forceUpdateInfo); this.resize(); - $(window).resize(function resize() { - this.resize(); - }.bind(this)); + window.addEventListener('resize', this.handleResize); } componentDidUpdate() { if ($('.post-right__scroll')[0]) { @@ -61,6 +63,7 @@ export default class RhsThread extends React.Component { PostStore.removeSelectedPostChangeListener(this.onChange); PostStore.removeChangeListener(this.onChangeAll); PreferenceStore.removeChangeListener(this.forceUpdateInfo); + window.removeEventListener('resize', this.handleResize); } forceUpdateInfo() { if (this.state.postList) { @@ -71,9 +74,15 @@ export default class RhsThread extends React.Component { } } } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } onChange() { var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } @@ -103,15 +112,15 @@ export default class RhsThread extends React.Component { } var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } resize() { - var height = $(window).height() - $('#error_bar').outerHeight() - 100; + var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100; $('.post-right__scroll').css('height', height + 'px'); $('.post-right__scroll').scrollTop(100000); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('.post-right__scroll').perfectScrollbar(); $('.post-right__scroll').perfectScrollbar('update'); } diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index e55fd3752..30e15d0ad 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -4,7 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var SearchBox = require('./search_bar.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SearchResultsHeader = require('./search_results_header.jsx'); var SearchResultsItem = require('./search_results_item.jsx'); @@ -20,18 +20,19 @@ export default class SearchResults extends React.Component { this.onChange = this.onChange.bind(this); this.resize = this.resize.bind(this); + this.handleResize = this.handleResize.bind(this); - this.state = getStateFromStores(); + const state = getStateFromStores(); + state.windowWidth = Utils.windowWidth(); + state.windowHeight = Utils.windowHeight(); + this.state = state; } componentDidMount() { this.mounted = true; PostStore.addSearchChangeListener(this.onChange); this.resize(); - var self = this; - $(window).resize(function resize() { - self.resize(); - }); + window.addEventListener('resize', this.handleResize); } componentDidUpdate() { @@ -41,22 +42,30 @@ export default class SearchResults extends React.Component { componentWillUnmount() { PostStore.removeSearchChangeListener(this.onChange); this.mounted = false; + window.removeEventListener('resize', this.handleResize); + } + + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); } onChange() { if (this.mounted) { var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } } resize() { - var height = $(window).height() - $('#error_bar').outerHeight() - 100; + var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100; $('#search-items-container').css('height', height + 'px'); $('#search-items-container').scrollTop(0); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('#search-items-container').perfectScrollbar(); } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 889bc0fbd..d1fe37300 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. const AsyncClient = require('../utils/async_client.jsx'); -const BrowserStore = require('../stores/browser_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); @@ -11,7 +10,6 @@ const NewChannelFlow = require('./new_channel_flow.jsx'); const MoreDirectChannels = require('./more_direct_channels.jsx'); const SearchBox = require('./search_bar.jsx'); const SidebarHeader = require('./sidebar_header.jsx'); -const SocketStore = require('../stores/socket_store.jsx'); const TeamStore = require('../stores/team_store.jsx'); const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); const UserStore = require('../stores/user_store.jsx'); @@ -31,9 +29,10 @@ export default class Sidebar extends React.Component { this.onChange = this.onChange.bind(this); this.onScroll = this.onScroll.bind(this); - this.onResize = this.onResize.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); + this.updateScrollbar = this.updateScrollbar.bind(this); + this.handleResize = this.handleResize.bind(this); this.showNewChannelModal = this.showNewChannelModal.bind(this); this.hideNewChannelModal = this.hideNewChannelModal.bind(this); @@ -48,6 +47,7 @@ export default class Sidebar extends React.Component { state.newChannelModalType = ''; state.showDirectChannelsModal = false; state.loadingDMChannel = -1; + state.windowWidth = Utils.windowWidth(); this.state = state; } @@ -129,15 +129,13 @@ export default class Sidebar extends React.Component { UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); - SocketStore.addChangeListener(this.onSocketChange); PreferenceStore.addChangeListener(this.onChange); - $('.nav-pills__container').perfectScrollbar(); - this.updateTitle(); this.updateUnreadIndicators(); + this.updateScrollbar(); - $(window).on('resize', this.onResize); + window.addEventListener('resize', this.handleResize); } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areStatesEqual(nextProps, this.props)) { @@ -152,111 +150,35 @@ export default class Sidebar extends React.Component { componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); + this.updateScrollbar(); } componentWillUnmount() { - $(window).off('resize', this.onResize); + window.removeEventListener('resize', this.handleResize); ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); - SocketStore.removeChangeListener(this.onSocketChange); PreferenceStore.removeChangeListener(this.onChange); } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } + updateScrollbar() { + if (this.state.windowWidth > 768) { + $('.nav-pills__container').perfectScrollbar(); + $('.nav-pills__container').perfectScrollbar('update'); + } + } onChange() { var newState = this.getStateFromStores(); if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } - onSocketChange(msg) { - if (msg.action === 'posted') { - if (ChannelStore.getCurrentId() === msg.channel_id) { - if (window.isActive) { - AsyncClient.updateLastViewedAt(); - } - } else { - AsyncClient.getChannels(); - } - - if (UserStore.getCurrentId() !== msg.user_id) { - var mentions = []; - if (msg.props.mentions) { - mentions = JSON.parse(msg.props.mentions); - } - var channel = ChannelStore.get(msg.channel_id); - - const user = UserStore.getCurrentUser(); - const member = ChannelStore.getMember(msg.channel_id); - - var notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; - if (notifyLevel === 'default') { - notifyLevel = user.notify_props.desktop; - } - - if (notifyLevel === 'none') { - return; - } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') { - return; - } - - var username = 'Someone'; - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - - var title = 'Posted'; - if (channel) { - title = channel.display_name; - } - - var repRegex = new RegExp('<br>', 'g'); - var post = JSON.parse(msg.props.post); - var msgProps = msg.props; - var notifyText = post.message.replace(repRegex, '\n').replace(/\n+/g, ' ').replace('<mention>', '').replace('</mention>', ''); - - if (notifyText.length > 50) { - notifyText = notifyText.substring(0, 49) + '...'; - } - - if (notifyText.length === 0) { - if (msgProps.image) { - Utils.notifyMe(title, username + ' uploaded an image', channel); - } else if (msgProps.otherFile) { - Utils.notifyMe(title, username + ' uploaded a file', channel); - } else { - Utils.notifyMe(title, username + ' did something new', channel); - } - } else { - Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); - } - if (!user.notify_props || user.notify_props.desktop_sound === 'true') { - Utils.ding(); - } - } - } else if (msg.action === 'viewed') { - if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } - } else if (msg.action === 'user_added') { - if (UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } - } else if (msg.action === 'user_removed') { - if (msg.user_id === UserStore.getCurrentId()) { - AsyncClient.getChannels(true); - - if (msg.props.remover !== msg.user_id && msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) { - var sentState = {}; - sentState.channelName = ChannelStore.getCurrent().display_name; - sentState.remover = UserStore.getProfile(msg.props.remover).username; - - BrowserStore.setItem('channel-removed-state', sentState); - $('#removed_from_channel').modal('show'); - } - } - } - } updateTitle() { const channel = ChannelStore.getCurrent(); if (channel) { @@ -276,9 +198,6 @@ export default class Sidebar extends React.Component { onScroll() { this.updateUnreadIndicators(); } - onResize() { - this.updateUnreadIndicators(); - } updateUnreadIndicators() { const container = $(ReactDOM.findDOMNode(this.refs.container)); diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index da0c1aaf9..540331663 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -86,11 +86,11 @@ export default class UserProfile extends React.Component { dataContent.push( <div data-toggle='tooltip' - title="' + this.state.profile.email + '" + title={this.state.profile.email} key='user-popover-email' > <a - href="mailto:' + this.state.profile.email + '" + href={'mailto:' + this.state.profile.email} className='text-nowrap text-lowercase user-popover__email' > {this.state.profile.email} diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx new file mode 100644 index 000000000..e83ae3bd6 --- /dev/null +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -0,0 +1,262 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Constants = require('../../utils/constants.jsx'); +var ChannelStore = require('../../stores/channel_store.jsx'); +var LoadingScreen = require('../loading_screen.jsx'); + +export default class ManageOutgoingHooks extends React.Component { + constructor() { + super(); + + this.getHooks = this.getHooks.bind(this); + this.addNewHook = this.addNewHook.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateCallbackURLs = this.updateCallbackURLs.bind(this); + + this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false}; + } + componentDidMount() { + this.getHooks(); + } + addNewHook(e) { + e.preventDefault(); + + if ((this.state.channelId === '' && this.state.triggerWords === '') || + this.state.callbackURLs === '') { + return; + } + + const hook = {}; + hook.channel_id = this.state.channelId; + if (this.state.triggerWords.length !== 0) { + hook.trigger_words = this.state.triggerWords.trim().split(','); + } + hook.callback_urls = this.state.callbackURLs.split('\n'); + + Client.addOutgoingHook( + hook, + (data) => { + let hooks = Object.assign([], this.state.hooks); + if (!hooks) { + hooks = []; + } + hooks.push(data); + this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''}); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + removeHook(id) { + const data = {}; + data.id = id; + + Client.deleteOutgoingHook( + data, + () => { + const hooks = this.state.hooks; + let index = -1; + for (let i = 0; i < hooks.length; i++) { + if (hooks[i].id === id) { + index = i; + break; + } + } + + if (index !== -1) { + hooks.splice(index, 1); + } + + this.setState({hooks}); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + regenToken(id) { + const regenData = {}; + regenData.id = id; + + Client.regenOutgoingHookToken( + regenData, + (data) => { + const hooks = Object.assign([], this.state.hooks); + for (let i = 0; i < hooks.length; i++) { + if (hooks[i].id === id) { + hooks[i] = data; + break; + } + } + + this.setState({hooks, serverError: null}); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + getHooks() { + Client.listOutgoingHooks( + (data) => { + if (data) { + this.setState({hooks: data, getHooksComplete: true, serverError: null}); + } + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + updateChannelId(e) { + this.setState({channelId: e.target.value}); + } + updateTriggerWords(e) { + this.setState({triggerWords: e.target.value}); + } + updateCallbackURLs(e) { + this.setState({callbackURLs: e.target.value}); + } + render() { + let serverError; + if (this.state.serverError) { + serverError = <label className='has-error'>{this.state.serverError}</label>; + } + + const channels = ChannelStore.getAll(); + const options = [<option value=''>{'--- Select a channel ---'}</option>]; + channels.forEach((channel) => { + if (channel.type === Constants.OPEN_CHANNEL) { + options.push(<option value={channel.id}>{channel.name}</option>); + } + }); + + const hooks = []; + this.state.hooks.forEach((hook) => { + const c = ChannelStore.get(hook.channel_id); + let channelDiv; + if (c) { + channelDiv = ( + <div className='padding-top'> + <strong>{'Channel: '}</strong>{c.name} + </div> + ); + } + + let triggerDiv; + if (hook.trigger_words && hook.trigger_words.length !== 0) { + triggerDiv = ( + <div className='padding-top'> + <strong>{'Trigger Words: '}</strong>{hook.trigger_words.join(', ')} + </div> + ); + } + + hooks.push( + <div className='font--small'> + <div className='padding-top x2 divider-light'></div> + <div className='padding-top x2'> + <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span> + </div> + {channelDiv} + {triggerDiv} + <div className='padding-top'> + <strong>{'Token: '}</strong>{hook.token} + </div> + <div className='padding-top'> + <a + className='text-danger' + href='#' + onClick={this.regenToken.bind(this, hook.id)} + > + {'Regen Token'} + </a> + <span>{' - '}</span> + <a + className='text-danger' + href='#' + onClick={this.removeHook.bind(this, hook.id)} + > + {'Remove'} + </a> + </div> + </div> + ); + }); + + let displayHooks; + if (!this.state.getHooksComplete) { + displayHooks = <LoadingScreen/>; + } else if (hooks.length > 0) { + displayHooks = hooks; + } else { + displayHooks = <label>{': None'}</label>; + } + + const existingHooks = ( + <div className='padding-top x2'> + <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label> + {displayHooks} + </div> + ); + + const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === ''; + + return ( + <div key='addOutgoingHook'> + <label className='control-label'>{'Add a new outgoing webhook'}</label> + <div className='padding-top'> + <strong>{'Channel:'}</strong> + <select + ref='channelName' + className='form-control' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + <span>{'Only public channels can be used'}</span> + <br/> + <br/> + <strong>{'Trigger Words:'}</strong> + <input + ref='triggerWords' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + placeholder='Optional if channel selected' + /> + <span>{'Comma separated words to trigger on'}</span> + <br/> + <br/> + <strong>{'Callback URLs:'}</strong> + <textarea + ref='callbackURLs' + className='form-control no-resize' + value={this.state.callbackURLs} + resize={false} + rows={3} + onChange={this.updateCallbackURLs} + /> + <span>{'New line separated URLs that will receive the HTTP POST event'}</span> + {serverError} + <div className='padding-top'> + <a + className={'btn btn-sm btn-primary'} + href='#' + disabled={disableButton} + onClick={this.addNewHook} + > + {'Add'} + </a> + </div> + </div> + {existingHooks} + </div> + ); + } +} diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 3be062ad3..231580cc3 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -4,6 +4,7 @@ var SettingItemMin = require('../setting_item_min.jsx'); var SettingItemMax = require('../setting_item_max.jsx'); var ManageIncomingHooks = require('./manage_incoming_hooks.jsx'); +var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx'); export default class UserSettingsIntegrationsTab extends React.Component { constructor(props) { @@ -19,6 +20,8 @@ export default class UserSettingsIntegrationsTab extends React.Component { } handleClose() { this.updateSection(''); + $('.ps-container.modal-body').scrollTop(0); + $('.ps-container.modal-body').perfectScrollbar('update'); } componentDidMount() { $('#user_settings').on('hidden.bs.modal', this.handleClose); @@ -28,35 +31,67 @@ export default class UserSettingsIntegrationsTab extends React.Component { } render() { let incomingHooksSection; + let outgoingHooksSection; var inputs = []; - if (this.props.activeSection === 'incoming-hooks') { - inputs.push( - <ManageIncomingHooks /> - ); + if (global.window.config.EnableIncomingWebhooks === 'true') { + if (this.props.activeSection === 'incoming-hooks') { + inputs.push( + <ManageIncomingHooks /> + ); - incomingHooksSection = ( - <SettingItemMax - title='Incoming Webhooks' - width = 'full' - inputs={inputs} - updateSection={function clearSection(e) { - this.updateSection(''); - e.preventDefault(); - }.bind(this)} - /> - ); - } else { - incomingHooksSection = ( - <SettingItemMin - title='Incoming Webhooks' - width = 'full' - describe='Manage your incoming webhooks (Developer feature)' - updateSection={function updateNameSection() { - this.updateSection('incoming-hooks'); - }.bind(this)} - /> - ); + incomingHooksSection = ( + <SettingItemMax + title='Incoming Webhooks' + width = 'full' + inputs={inputs} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + incomingHooksSection = ( + <SettingItemMin + title='Incoming Webhooks' + width = 'full' + describe='Manage your incoming webhooks (Developer feature)' + updateSection={() => { + this.updateSection('incoming-hooks'); + }} + /> + ); + } + } + + if (global.window.config.EnableOutgoingWebhooks === 'true') { + if (this.props.activeSection === 'outgoing-hooks') { + inputs.push( + <ManageOutgoingHooks /> + ); + + outgoingHooksSection = ( + <SettingItemMax + title='Outgoing Webhooks' + inputs={inputs} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + outgoingHooksSection = ( + <SettingItemMin + title='Outgoing Webhooks' + describe='Manage your outgoing webhooks' + updateSection={() => { + this.updateSection('outgoing-hooks'); + }} + /> + ); + } } return ( @@ -82,6 +117,8 @@ export default class UserSettingsIntegrationsTab extends React.Component { <h3 className='tab-header'>{'Integration Settings'}</h3> <div className='divider-dark first'/> {incomingHooksSection} + <div className='divider-light'/> + {outgoingHooksSection} <div className='divider-dark'/> </div> </div> diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 692fb26ee..44cd423b5 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -38,7 +38,7 @@ export default class UserSettingsModal extends React.Component { if (global.window.config.EnableOAuthServiceProvider === 'true') { tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); } - if (global.window.config.EnableIncomingWebhooks === 'true') { + if (global.window.config.EnableIncomingWebhooks === 'true' || global.window.config.EnableOutgoingWebhooks === 'true') { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index f75693470..322e68c17 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -6,6 +6,7 @@ const Utils = require('../utils/utils.jsx'); const Constants = require('../utils/constants.jsx'); const ViewImagePopoverBar = require('./view_image_popover_bar.jsx'); const Modal = ReactBootstrap.Modal; +const KeyCodes = Constants.KeyCodes; export default class ViewImageModal extends React.Component { constructor(props) { @@ -63,11 +64,11 @@ export default class ViewImageModal extends React.Component { this.loadImage(id); } handleKeyPress(e) { - if (!e) { + if (!e || !this.props.show) { return; - } else if (e.keyCode === 39) { + } else if (e.keyCode === KeyCodes.RIGHT) { this.handleNext(); - } else if (e.keyCode === 37) { + } else if (e.keyCode === KeyCodes.LEFT) { this.handlePrev(); } } diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx index c89cb4edc..ea9ae06f4 100644 --- a/web/react/pages/admin_console.jsx +++ b/web/react/pages/admin_console.jsx @@ -5,9 +5,12 @@ var ErrorBar = require('../components/error_bar.jsx'); var SelectTeamModal = require('../components/admin_console/select_team_modal.jsx'); var AdminController = require('../components/admin_console/admin_controller.jsx'); -export function setupAdminConsolePage() { +export function setupAdminConsolePage(props) { ReactDOM.render( - <AdminController />, + <AdminController + tab={props.ActiveTab} + teamId={props.TeamId} + />, document.getElementById('admin_controller') ); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 77e7067ad..77951f214 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -1,15 +1,22 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var UserStore = require('./user_store.jsx'); -var ErrorStore = require('./error_store.jsx'); -var EventEmitter = require('events').EventEmitter; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const UserStore = require('./user_store.jsx'); +const PostStore = require('./post_store.jsx'); +const ChannelStore = require('./channel_store.jsx'); +const BrowserStore = require('./browser_store.jsx'); +const ErrorStore = require('./error_store.jsx'); +const EventEmitter = require('events').EventEmitter; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const Utils = require('../utils/utils.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); -var CHANGE_EVENT = 'change'; +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const SocketEvents = Constants.SocketEvents; + +const CHANGE_EVENT = 'change'; var conn; @@ -94,6 +101,39 @@ class SocketStoreClass extends EventEmitter { removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + handleMessage(msg) { + switch (msg.action) { + case SocketEvents.POSTED: + handleNewPostEvent(msg); + break; + + case SocketEvents.POST_EDITED: + handlePostEditEvent(msg); + break; + + case SocketEvents.POST_DELETED: + handlePostDeleteEvent(msg); + break; + + case SocketEvents.NEW_USER: + handleNewUserEvent(); + break; + + case SocketEvents.USER_ADDED: + handleUserAddedEvent(msg); + break; + + case SocketEvents.USER_REMOVED: + handleUserRemovedEvent(msg); + break; + + case SocketEvents.CHANNEL_VIEWED: + handleChannelViewedEvent(msg); + break; + + default: + } + } sendMessage(msg) { if (conn && conn.readyState === WebSocket.OPEN) { conn.send(JSON.stringify(msg)); @@ -104,6 +144,138 @@ class SocketStoreClass extends EventEmitter { } } +function handleNewPostEvent(msg) { + // Store post + const post = JSON.parse(msg.props.post); + PostStore.storePost(post); + + // Update channel state + if (ChannelStore.getCurrentId() === msg.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } + } else { + AsyncClient.getChannel(msg.channel_id); + } + + // Send desktop notification + if (UserStore.getCurrentId() !== msg.user_id) { + const msgProps = msg.props; + + let mentions = []; + if (msgProps.mentions) { + mentions = JSON.parse(msg.props.mentions); + } + + const channel = ChannelStore.get(msg.channel_id); + const user = UserStore.getCurrentUser(); + const member = ChannelStore.getMember(msg.channel_id); + + let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; + if (notifyLevel === 'default') { + notifyLevel = user.notify_props.desktop; + } + + if (notifyLevel === 'none') { + return; + } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') { + return; + } + + let username = 'Someone'; + if (UserStore.hasProfile(msg.user_id)) { + username = UserStore.getProfile(msg.user_id).username; + } + + let title = 'Posted'; + if (channel) { + title = channel.display_name; + } + + let notifyText = post.message.replace(/\n+/g, ' '); + if (notifyText.length > 50) { + notifyText = notifyText.substring(0, 49) + '...'; + } + + if (notifyText.length === 0) { + if (msgProps.image) { + Utils.notifyMe(title, username + ' uploaded an image', channel); + } else if (msgProps.otherFile) { + Utils.notifyMe(title, username + ' uploaded a file', channel); + } else { + Utils.notifyMe(title, username + ' did something new', channel); + } + } else { + Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); + } + if (!user.notify_props || user.notify_props.desktop_sound === 'true') { + Utils.ding(); + } + } +} + +function handlePostEditEvent(msg) { + // Store post + const post = JSON.parse(msg.props.post); + PostStore.storePost(post); + + // Update channel state + if (ChannelStore.getCurrentId() === msg.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } + } +} + +function handlePostDeleteEvent(msg) { + const post = JSON.parse(msg.props.post); + + PostStore.storeUnseenDeletedPost(post); + PostStore.removePost(post, true); + PostStore.emitChange(); +} + +function handleNewUserEvent() { + AsyncClient.getProfiles(); + AsyncClient.getChannelExtraInfo(true); +} + +function handleUserAddedEvent(msg) { + if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(true); + } + + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + +function handleUserRemovedEvent(msg) { + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannels(); + + if (msg.props.remover_id !== msg.user_id && + msg.channel_id === ChannelStore.getCurrentId() && + $('#removed_from_channel').length > 0) { + var sentState = {}; + sentState.channelName = ChannelStore.getCurrent().display_name; + sentState.remover = UserStore.getProfile(msg.props.remover_id).username; + + BrowserStore.setItem('channel-removed-state', sentState); + $('#removed_from_channel').modal('show'); + } + } else if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(true); + } +} + +function handleChannelViewedEvent(msg) { + // Useful for when multiple devices have the app open to different channels + if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + var SocketStore = new SocketStoreClass(); SocketStore.dispatchToken = AppDispatcher.register((payload) => { @@ -111,6 +283,7 @@ SocketStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECIEVED_MSG: + SocketStore.handleMessage(action.msg); SocketStore.emitChange(action.msg); break; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index f6aee362c..f92633439 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1182,3 +1182,61 @@ export function savePreferences(preferences, success, error) { } }); } + +export function addOutgoingHook(hook, success, error) { + $.ajax({ + url: '/api/v1/hooks/outgoing/create', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(hook), + success, + error: (xhr, status, err) => { + var e = handleError('addOutgoingHook', xhr, status, err); + error(e); + } + }); +} + +export function deleteOutgoingHook(data, success, error) { + $.ajax({ + url: '/api/v1/hooks/outgoing/delete', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('deleteOutgoingHook', xhr, status, err); + error(e); + } + }); +} + +export function listOutgoingHooks(success, error) { + $.ajax({ + url: '/api/v1/hooks/outgoing/list', + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('listOutgoingHooks', xhr, status, err); + error(e); + } + }); +} + +export function regenOutgoingHookToken(data, success, error) { + $.ajax({ + url: '/api/v1/hooks/outgoing/regen_token', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('regenOutgoingHookToken', xhr, status, err); + error(e); + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b7b8d3c60..1d856e067 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -47,6 +47,18 @@ module.exports = { SERVER_ACTION: null, VIEW_ACTION: null }), + + SocketEvents: { + POSTED: 'posted', + POST_EDITED: 'post_edited', + POST_DELETED: 'post_deleted', + CHANNEL_VIEWED: 'channel_viewed', + NEW_USER: 'new_user', + USER_ADDED: 'user_added', + USER_REMOVED: 'user_removed', + TYPING: 'typing' + }, + SPECIAL_MENTIONS: ['all', 'channel'], CHARACTER_LIMIT: 4000, IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'], @@ -114,6 +126,7 @@ module.exports = { MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MAX_DMS: 20, DM_CHANNEL: 'D', + OPEN_CHANNEL: 'O', MAX_POST_LEN: 4000, EMOJI_SIZE: 16, ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 38ac68d58..5f266bba3 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -519,11 +519,11 @@ export function applyTheme(theme) { changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); - changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); @@ -968,3 +968,11 @@ export function getShortenedTeamURL() { } return teamURL + '/'; } + +export function windowWidth() { + return $(window).width(); +} + +export function windowHeight() { + return $(window).height(); +} |