summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/activity_log_modal.jsx10
-rw-r--r--web/react/components/admin_console/admin_controller.jsx12
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/email_settings.jsx8
-rw-r--r--web/react/components/admin_console/log_settings.jsx18
-rw-r--r--web/react/components/admin_console/service_settings.jsx36
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/channel_header.jsx16
-rw-r--r--web/react/components/create_comment.jsx39
-rw-r--r--web/react/components/create_post.jsx23
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/file_attachment.jsx158
-rw-r--r--web/react/components/file_upload_overlay.jsx16
-rw-r--r--web/react/components/more_channels.jsx6
-rw-r--r--web/react/components/more_direct_channels.jsx77
-rw-r--r--web/react/components/msg_typing.jsx15
-rw-r--r--web/react/components/popover_list_members.jsx24
-rw-r--r--web/react/components/post_body.jsx8
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx60
-rw-r--r--web/react/components/rhs_comment.jsx26
-rw-r--r--web/react/components/rhs_thread.jsx27
-rw-r--r--web/react/components/search_results.jsx27
-rw-r--r--web/react/components/sidebar.jsx139
-rw-r--r--web/react/components/team_signup_url_page.jsx10
-rw-r--r--web/react/components/team_signup_username_page.jsx7
-rw-r--r--web/react/components/user_profile.jsx41
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx262
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx10
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx21
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx89
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx65
-rw-r--r--web/react/components/view_image.jsx96
-rw-r--r--web/react/pages/admin_console.jsx7
-rw-r--r--web/react/stores/post_store.jsx13
-rw-r--r--web/react/stores/socket_store.jsx187
-rw-r--r--web/react/utils/client.jsx58
-rw-r--r--web/react/utils/constants.jsx13
-rw-r--r--web/react/utils/emoticons.jsx1
-rw-r--r--web/react/utils/utils.jsx16
-rw-r--r--web/sass-files/sass/partials/_command-box.scss1
-rw-r--r--web/sass-files/sass/partials/_files.scss39
-rw-r--r--web/sass-files/sass/partials/_modal.scss91
-rw-r--r--web/sass-files/sass/partials/_post.scss48
-rw-r--r--web/sass-files/sass/partials/_responsive.scss33
-rw-r--r--web/sass-files/sass/partials/_settings.scss14
-rw-r--r--web/sass-files/sass/partials/_videos.scss7
-rw-r--r--web/static/images/filesOverlay.pngbin0 -> 8392 bytes
-rw-r--r--web/static/images/logoWhite.pngbin0 -> 5876 bytes
-rw-r--r--web/static/images/webhook_icon.jpgbin0 -> 68190 bytes
-rw-r--r--web/templates/admin_console.html2
-rw-r--r--web/web.go45
53 files changed, 1471 insertions, 496 deletions
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 74d6c64e3..2c944913f 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -81,6 +81,7 @@ export default class ActivityLogModal extends React.Component {
const currentSession = this.state.sessions[i];
const lastAccessTime = new Date(currentSession.last_activity_at);
const firstAccessTime = new Date(currentSession.create_at);
+ let devicePlatform = currentSession.props.platform;
let devicePicture = '';
if (currentSession.props.platform === 'Windows') {
@@ -88,7 +89,12 @@ export default class ActivityLogModal extends React.Component {
} else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') {
devicePicture = 'fa fa-apple';
} else if (currentSession.props.platform === 'Linux') {
- devicePicture = 'fa fa-linux';
+ if (currentSession.props.os.indexOf('Android') >= 0) {
+ devicePlatform = 'Android';
+ devicePicture = 'fa fa-android';
+ } else {
+ devicePicture = 'fa fa-linux';
+ }
}
let moreInfo;
@@ -119,7 +125,7 @@ export default class ActivityLogModal extends React.Component {
className='activity-log__table'
>
<div className='activity-log__report'>
- <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div>
+ <div className='report__platform'><i className={devicePicture} />{devicePlatform}</div>
<div className='report__info'>
<div>{`Last activity: ${lastAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div>
{moreInfo}
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/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 01759b222..40e00ff04 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -440,9 +440,11 @@ export default class EmailSettings extends React.Component {
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
- <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
- <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </tbody>
</table>
</div>
<div className='help-text'>
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 931818bb8..7e9eda89b 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -249,22 +249,24 @@ export default class LogSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.fileEnable}
/>
- <p className='help-text'>
+ <div className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
<div className='help-text'>
<table
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
- <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
- <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
- <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
- <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
- <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
+ <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
+ <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
+ <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
+ <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
+ <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ </tbody>
</table>
</div>
- </p>
+ </div>
</div>
</div>
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/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index da4299714..9ecd14a1e 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -31,6 +31,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
+ config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -209,6 +210,39 @@ export default class TeamSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictTeamNames'
+ >
+ {'Restrict Team Names: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='true'
+ ref='RestrictTeamNames'
+ defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
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..435c7d542 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -13,8 +13,10 @@ const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
const FilePreview = require('./file_preview.jsx');
const Utils = require('../utils/utils.jsx');
+
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class CreateComment extends React.Component {
constructor(props) {
@@ -25,6 +27,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleArrowUp = this.handleArrowUp.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -32,6 +35,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 +44,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');
}
}
@@ -147,6 +161,26 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
+ handleArrowUp(e) {
+ if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ e.preventDefault();
+
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId);
+ if (!lastPost) {
+ return;
+ }
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.RECIEVED_EDIT_POST,
+ refocusId: '#reply_textbox',
+ title: 'Comment',
+ message: lastPost.message,
+ postId: lastPost.id,
+ channelId: lastPost.channel_id
+ });
+ }
+ }
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -279,6 +313,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
+ onKeyDown={this.handleArrowUp}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
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/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 90d9696e7..b259b3c18 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -70,7 +70,7 @@ export default class EditPostModal extends React.Component {
refocusId: options.refocusId || ''
});
- $(React.findDOMNode(this.refs.modal)).modal('show');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
componentDidMount() {
var self = this;
@@ -92,7 +92,7 @@ export default class EditPostModal extends React.Component {
$('#edit_textbox').get(0).focus();
});
- $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
if (self.state.refocusId !== '') {
setTimeout(() => {
$(self.state.refocusId).get(0).focus();
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c6dff6550..57cccc4e0 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -10,9 +10,12 @@ export default class FileAttachment extends React.Component {
super(props);
this.loadFiles = this.loadFiles.bind(this);
+ this.playGif = this.playGif.bind(this);
+ this.stopGif = this.stopGif.bind(this);
+ this.addBackgroundImage = this.addBackgroundImage.bind(this);
this.canSetState = false;
- this.state = {fileSize: -1};
+ this.state = {fileSize: -1, mime: '', playing: false, loading: false, format: ''};
}
componentDidMount() {
this.loadFiles();
@@ -28,15 +31,9 @@ export default class FileAttachment extends React.Component {
var filename = this.props.filename;
if (filename) {
- var fileInfo = utils.splitFileLocation(filename);
+ var fileInfo = this.getFileInfoFromName(filename);
var type = utils.getFileType(fileInfo.ext);
- // This is a temporary patch to fix issue with old files using absolute paths
- if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
- fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
- }
- fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
-
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
@@ -58,11 +55,7 @@ export default class FileAttachment extends React.Component {
$(imgDiv).addClass('normal');
}
- var re1 = new RegExp(' ', 'g');
- var re2 = new RegExp('\\(', 'g');
- var re3 = new RegExp('\\)', 'g');
- var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
- $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
+ self.addBackgroundImage(name, path);
}
};
}(fileInfo.path, filename));
@@ -93,6 +86,75 @@ export default class FileAttachment extends React.Component {
return true;
}
+ playGif(e, filename) {
+ var img = new Image();
+ var fileUrl = utils.getFileUrl(filename);
+
+ this.setState({loading: true});
+ img.load(fileUrl);
+ img.onload = () => {
+ var state = {playing: true, loading: false};
+
+ switch (true) {
+ case img.width > img.height:
+ state.format = 'landscape';
+ break;
+ case img.height > img.width:
+ state.format = 'portrait';
+ break;
+ default:
+ state.format = 'quadrat';
+ break;
+ }
+
+ this.setState(state);
+
+ // keep displaying background image for a short moment while browser is
+ // loading gif, to prevent white background flashing through
+ setTimeout(() => this.removeBackgroundImage.bind(this)(filename), 100);
+ };
+ img.onError = () => this.setState({loading: false});
+
+ e.stopPropagation();
+ }
+ stopGif(e, filename) {
+ this.setState({playing: false});
+ this.addBackgroundImage(filename);
+ e.stopPropagation();
+ }
+ getFileInfoFromName(name) {
+ var fileInfo = utils.splitFileLocation(name);
+
+ // This is a temporary patch to fix issue with old files using absolute paths
+ if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
+ fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
+ }
+ fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
+
+ return fileInfo;
+ }
+ addBackgroundImage(name, path) {
+ var fileUrl = path;
+
+ if (name in this.refs) {
+ if (!path) {
+ fileUrl = this.getFileInfoFromName(name).path;
+ }
+
+ var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
+ var re1 = new RegExp(' ', 'g');
+ var re2 = new RegExp('\\(', 'g');
+ var re3 = new RegExp('\\)', 'g');
+ var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
+
+ $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
+ }
+ }
+ removeBackgroundImage(name) {
+ if (name in this.refs) {
+ $(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
+ }
+ }
render() {
var filename = this.props.filename;
@@ -100,15 +162,71 @@ export default class FileAttachment extends React.Component {
var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
- var thumbnail;
- if (type === 'image') {
- thumbnail = (
+ var playbackControls = '';
+ var loadedFile = '';
+ var loadingIndicator = '';
+ if (this.state.mime === 'image/gif') {
+ playbackControls = (
<div
- ref={filename}
- className='post__load'
- style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ if (this.state.playing) {
+ loadedFile = (
+ <img
+ className={'file__loaded ' + this.state.format}
+ src={fileUrl}
+ />
+ );
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ }
+ if (this.state.loading) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
/>
);
+ playbackControls = '';
+ }
+
+ var thumbnail;
+ if (type === 'image') {
+ if (this.state.playing) {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ } else {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {loadingIndicator}
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ }
} else {
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
}
@@ -119,7 +237,7 @@ export default class FileAttachment extends React.Component {
filename,
function success(data) {
if (this.canSetState) {
- this.setState({fileSize: parseInt(data.size, 10)});
+ this.setState({fileSize: parseInt(data.size, 10), mime: data.mime});
}
}.bind(this),
function error() {}
diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx
index 4fcee6cb0..d991dd625 100644
--- a/web/react/components/file_upload_overlay.jsx
+++ b/web/react/components/file_upload_overlay.jsx
@@ -12,9 +12,19 @@ export default class FileUploadOverlay extends React.Component {
return (
<div className={overlayClass}>
- <div>
- <i className='fa fa-upload'></i>
- <span>Drop a file to upload it.</span>
+ <div className='overlay__circle'>
+ <img
+ className='overlay__files'
+ src='/static/images/filesOverlay.png'
+ alt='Files'
+ />
+ <span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span>
+ <img
+ className='overlay__logo'
+ src='/static/images/logoWhite.png'
+ width='100'
+ alt='Logo'
+ />
</div>
</div>
);
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/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 16ae693fa..155e88600 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -35,13 +35,20 @@ export default class PopoverListMembers extends React.Component {
const teamMembers = UserStore.getProfilesUsernameMap();
if (members && teamMembers) {
- members.sort(function compareByLocal(a, b) {
+ members.sort((a, b) => {
return a.username.localeCompare(b.username);
});
- members.forEach(function addMemberElement(m) {
+ members.forEach((m, i) => {
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
- popoverHtml.push(<div className='text--nowrap'>{m.username}</div>);
+ popoverHtml.push(
+ <div
+ className='text--nowrap'
+ key={'popover-member-' + i}
+ >
+ {m.username}
+ </div>
+ );
count++;
}
});
@@ -57,8 +64,15 @@ export default class PopoverListMembers extends React.Component {
<OverlayTrigger
trigger='click'
placement='bottom'
- rootClose='true'
- overlay={<Popover title='Members'>{popoverHtml}</Popover>}
+ rootClose={true}
+ overlay={
+ <Popover
+ title='Members'
+ id='member-list-popover'
+ >
+ {popoverHtml}
+ </Popover>
+ }
>
<div id='member_popover'>
<div>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 1db0b12e7..fb838b736 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -116,7 +116,7 @@ export default class PostBody extends React.Component {
}
var metadata = data.items[0].snippet;
this.receivedYoutubeData = true;
- this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title});
+ this.setState({youtubeTitle: metadata.title});
}
if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) {
@@ -134,18 +134,12 @@ export default class PostBody extends React.Component {
header = header + ' - ';
}
- let uploader = this.state.youtubeUploader;
- if (!uploader) {
- uploader = 'unknown';
- }
-
return (
<div className='post-comment'>
<h4>
<span className='video-type'>{header}</span>
<span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
</h4>
- <h4 className='video-uploader'>{uploader}</h4>
<div
className='video-div embed-responsive-item'
id={youtubeId}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index a95095ff6..36260d77c 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,7 +150,7 @@ export default class PostInfo extends React.Component {
<ul className='post-header post-info'>
<li className='post-header-col'>
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
container={this}
placement='top'
overlay={tooltip}
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_comment.jsx b/web/react/components/rhs_comment.jsx
index 402e64080..d3a4cfaeb 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -29,7 +29,7 @@ export default class RhsComment extends React.Component {
var post = this.props.post;
Client.createPost(post, post.channel_id,
- function success(data) {
+ (data) => {
AsyncClient.getPosts(post.channel_id);
var channel = ChannelStore.get(post.channel_id);
@@ -43,11 +43,11 @@ export default class RhsComment extends React.Component {
post: data
});
},
- function fail() {
+ () => {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
this.forceUpdate();
- }.bind(this)
+ }
);
post.state = Constants.POST_LOADING;
@@ -84,7 +84,10 @@ export default class RhsComment extends React.Component {
if (isOwner) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='edit-button'
+ >
<a
href='#'
role='menuitem'
@@ -95,7 +98,7 @@ export default class RhsComment extends React.Component {
data-postid={post.id}
data-channelid={post.channel_id}
>
- Edit
+ {'Edit'}
</a>
</li>
);
@@ -103,7 +106,10 @@ export default class RhsComment extends React.Component {
if (isOwner || isAdmin) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='delete-button'
+ >
<a
href='#'
role='menuitem'
@@ -114,7 +120,7 @@ export default class RhsComment extends React.Component {
data-channelid={post.channel_id}
data-comments={0}
>
- Delete
+ {'Delete'}
</a>
</li>
);
@@ -162,7 +168,7 @@ export default class RhsComment extends React.Component {
href='#'
onClick={this.retryComment}
>
- Retry
+ {'Retry'}
</a>
);
} else if (post.state === Constants.POST_LOADING) {
@@ -213,14 +219,14 @@ export default class RhsComment extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p className={postClass}>
+ <div className={postClass}>
{loading}
<div
ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>
- </p>
+ </div>
{fileAttachment}
</div>
</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 89506c028..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);
@@ -46,8 +45,9 @@ export default class Sidebar extends React.Component {
const state = this.getStateFromStores();
state.newChannelModalType = '';
- state.showMoreDirectChannelsModal = false;
+ 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));
@@ -471,11 +390,13 @@ export default class Sidebar extends React.Component {
}
let closeButton = null;
- const removeTooltip = <Tooltip>{'Remove from list'}</Tooltip>;
+ const removeTooltip = (
+ <Tooltip id='remove-dm-tooltip'>{'Remove from list'}</Tooltip>
+ );
if (handleClose && !badge) {
closeButton = (
<OverlayTrigger
- delayShow='1000'
+ delayShow={1000}
placement='top'
overlay={removeTooltip}
>
@@ -564,8 +485,12 @@ export default class Sidebar extends React.Component {
showChannelModal = true;
}
- const createChannelTootlip = <Tooltip>{'Create new channel'}</Tooltip>;
- const createGroupTootlip = <Tooltip>{'Create new group'}</Tooltip>;
+ const createChannelTootlip = (
+ <Tooltip id='new-channel-tooltip' >{'Create new channel'}</Tooltip>
+ );
+ const createGroupTootlip = (
+ <Tooltip id='new-group-tooltip'>{'Create new group'}</Tooltip>
+ );
return (
<div>
@@ -607,7 +532,7 @@ export default class Sidebar extends React.Component {
<h4>
{'Channels'}
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
placement='top'
overlay={createChannelTootlip}
>
@@ -640,7 +565,7 @@ export default class Sidebar extends React.Component {
<h4>
{'Private Groups'}
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
placement='top'
overlay={createGroupTootlip}
>
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 67e4c9dd7..75ec2dfd9 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -40,10 +40,12 @@ export default class TeamSignupUrlPage extends React.Component {
return;
}
- for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
- if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: 'URL is taken or contains a reserved word'});
- return;
+ if (global.window.config.RestrictTeamNames === 'true') {
+ for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
+ if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
+ this.setState({nameError: 'URL is taken or contains a reserved word'});
+ return;
+ }
}
}
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index fa8a031a0..21e76e2b8 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -15,7 +15,12 @@ export default class TeamSignupUsernamePage extends React.Component {
}
submitBack(e) {
e.preventDefault();
- this.props.state.wizard = 'send_invites';
+ if (global.window.config.SendEmailNotifications === 'true') {
+ this.props.state.wizard = 'send_invites';
+ } else {
+ this.props.state.wizard = 'team_url';
+ }
+
this.props.updateParent(this.props.state);
}
submitNext(e) {
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 715161b4f..540331663 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -65,22 +65,32 @@ export default class UserProfile extends React.Component {
var dataContent = [];
dataContent.push(
- <img className='user-popover__image'
+ <img
+ className='user-popover__image'
src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at}
height='128'
width='128'
+ key='user-popover-image'
/>
);
if (!global.window.config.ShowEmailAddress === 'true') {
- dataContent.push(<div className='text-nowrap'>{'Email not shared'}</div>);
+ dataContent.push(
+ <div
+ className='text-nowrap'
+ key='user-popover-no-email'
+ >
+ {'Email not shared'}
+ </div>
+ );
} else {
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}
@@ -93,15 +103,22 @@ export default class UserProfile extends React.Component {
<OverlayTrigger
trigger='click'
placement='right'
- rootClose='true'
- overlay={<Popover title={this.state.profile.username}>{dataContent}</Popover>}
- >
- <div
- className='user-popover'
- id={'profile_' + this.uniqueId}
+ rootClose={true}
+ overlay={
+ <Popover
+ title={this.state.profile.username}
+ id='user-profile-popover'
+ >
+ {dataContent}
+ </Popover>
+ }
>
- {name}
- </div>
+ <div
+ className='user-popover'
+ id={'profile_' + this.uniqueId}
+ >
+ {name}
+ </div>
</OverlayTrigger>
);
}
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_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 7f363e92e..8c62a189d 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -152,9 +152,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
- >
- {'Theme Colors'}
- </input>
+ />
+ {'Theme Colors'}
</label>
<br/>
</div>
@@ -164,9 +163,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
- >
- {'Custom Theme'}
- </input>
+ />
+ {'Custom Theme'}
</label>
<br/>
</div>
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index ec209c218..22a62273c 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import { savePreferences } from '../../utils/client.jsx';
+import {savePreferences} from '../../utils/client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
@@ -38,7 +38,7 @@ export default class UserSettingsDisplay extends React.Component {
);
}
handleClockRadio(militaryTime) {
- this.setState({militaryTime: militaryTime});
+ this.setState({militaryTime});
}
updateSection(section) {
this.setState(getDisplayStateFromStores());
@@ -57,7 +57,7 @@ export default class UserSettingsDisplay extends React.Component {
const serverError = this.state.serverError || null;
let clockSection;
if (this.props.activeSection === 'clock') {
- let clockFormat = [false, false];
+ const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
clockFormat[1] = true;
} else {
@@ -77,9 +77,8 @@ export default class UserSettingsDisplay extends React.Component {
type='radio'
checked={clockFormat[0]}
onChange={this.handleClockRadio.bind(this, 'false')}
- >
- 12-hour clock (example: 4:00 PM)
- </input>
+ />
+ {'12-hour clock (example: 4:00 PM)'}
</label>
<br/>
</div>
@@ -89,9 +88,8 @@ export default class UserSettingsDisplay extends React.Component {
type='radio'
checked={clockFormat[1]}
onChange={this.handleClockRadio.bind(this, 'true')}
- >
- 24-hour clock (example: 16:00)
- </input>
+ />
+ {'24-hour clock (example: 16:00)'}
</label>
<br/>
</div>
@@ -99,7 +97,6 @@ export default class UserSettingsDisplay extends React.Component {
</div>
];
-
clockSection = (
<SettingItemMax
title='Clock Display'
@@ -138,13 +135,13 @@ export default class UserSettingsDisplay extends React.Component {
className='close'
data-dismiss='modal'
aria-label='Close'
- >
+ >
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
- >
+ >
<i className='modal-back'></i>
{'Display Settings'}
</h4>
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/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 4dbb9b96f..8693af494 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -228,9 +228,8 @@ export default class NotificationsTab extends React.Component {
<input type='radio'
checked={notifyActive[0]}
onChange={this.handleNotifyRadio.bind(this, 'all')}
- >
- For all activity
- </input>
+ />
+ {'For all activity'}
</label>
<br/>
</div>
@@ -240,9 +239,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
- >
- Only for mentions and direct messages
- </input>
+ />
+ {'Only for mentions and direct messages'}
</label>
<br/>
</div>
@@ -252,9 +250,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleNotifyRadio.bind(this, 'none')}
- >
- Never
- </input>
+ />
+ {'Never'}
</label>
</div>
</div>
@@ -320,9 +317,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[0]}
onChange={this.handleSoundRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -332,9 +328,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[1]}
onChange={this.handleSoundRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
@@ -402,9 +397,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -414,9 +408,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
@@ -482,9 +475,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.firstNameKey}
onChange={handleUpdateFirstNameKey}
- >
- {'Your case sensitive first name "' + user.first_name + '"'}
- </input>
+ />
+ {'Your case sensitive first name "' + user.first_name + '"'}
</label>
</div>
</div>
@@ -502,9 +494,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.usernameKey}
onChange={handleUpdateUsernameKey}
- >
- {'Your non-case sensitive username "' + user.username + '"'}
- </input>
+ />
+ {'Your non-case sensitive username "' + user.username + '"'}
</label>
</div>
</div>
@@ -521,9 +512,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.mentionKey}
onChange={handleUpdateMentionKey}
- >
- {'Your username mentioned "@' + user.username + '"'}
- </input>
+ />
+ {'Your username mentioned "@' + user.username + '"'}
</label>
</div>
</div>
@@ -540,9 +530,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.allKey}
onChange={handleUpdateAllKey}
- >
- {'Team-wide mentions "@all"'}
- </input>
+ />
+ {'Team-wide mentions "@all"'}
</label>
</div>
</div>
@@ -559,9 +548,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.channelKey}
onChange={handleUpdateChannelKey}
- >
- {'Channel-wide mentions "@channel"'}
- </input>
+ />
+ {'Channel-wide mentions "@channel"'}
</label>
</div>
</div>
@@ -576,9 +564,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.customKeysChecked}
onChange={this.updateCustomMentionKeys}
- >
- {'Other non-case sensitive words, separated by commas:'}
- </input>
+ />
+ {'Other non-case sensitive words, separated by commas:'}
</label>
</div>
<input
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index f75693470..bea6ce7a5 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) {
@@ -37,7 +38,10 @@ export default class ViewImageModal extends React.Component {
progress: progress,
images: {},
fileSizes: {},
- showFooter: false
+ fileMimes: {},
+ showFooter: false,
+ isPlaying: {},
+ isLoading: {}
};
}
handleNext(e) {
@@ -63,11 +67,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();
}
}
@@ -121,6 +125,36 @@ export default class ViewImageModal extends React.Component {
this.setState({loaded});
}
}
+ playGif(e, filename, fileUrl) {
+ var isLoading = this.state.isLoading;
+ var isPlaying = this.state.isPlaying;
+
+ isLoading[filename] = fileUrl;
+ this.setState({isLoading});
+
+ var img = new Image();
+ img.load(fileUrl);
+ img.onload = () => {
+ delete isLoading[filename];
+ isPlaying[filename] = fileUrl;
+ this.setState({isPlaying, isLoading});
+ };
+ img.onError = () => {
+ delete isLoading[filename];
+ this.setState({isLoading});
+ };
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ stopGif(e, filename) {
+ var isPlaying = this.state.isPlaying;
+ delete isPlaying[filename];
+ this.setState({isPlaying});
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
componentDidMount() {
$(window).on('keyup', this.handleKeyPress);
@@ -153,6 +187,10 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (filename in this.state.isPlaying) {
+ return this.state.isPlaying[filename];
+ }
+
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
@@ -188,12 +226,62 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (!(filename in this.state.fileMimes)) {
+ Client.getFileInfo(
+ filename,
+ (data) => {
+ if (this.canSetState) {
+ var fileMimes = this.state.fileMimes;
+ fileMimes[filename] = data.mime;
+ this.setState(fileMimes);
+ }
+ },
+ () => {}
+ );
+ }
+
+ var playbackControls = '';
+ if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading)) {
+ if (filename in this.state.isPlaying) {
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ } else {
+ playbackControls = (
+ <div
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename, fileUrl)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ }
+
+ var loadingIndicator = '';
+ if (this.state.isLoading[filename] === fileUrl) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
+ />
+ );
+ playbackControls = '';
+ }
+
// image files just show a preview of the file
content = (
<a
href={fileUrl}
target='_blank'
>
+ {loadingIndicator}
+ {playbackControls}
<img
style={{maxHeight: this.state.imgHeight}}
ref='image'
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/post_store.jsx b/web/react/stores/post_store.jsx
index 8609d8bbf..0ace956d2 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -230,7 +230,7 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
- getCurrentUsersLatestPost(channelId) {
+ getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
var i = 0;
@@ -239,8 +239,15 @@ class PostStoreClass extends EventEmitter {
for (i; i < len; i++) {
if (postList.posts[postList.order[i]].user_id === userId) {
- lastPost = postList.posts[postList.order[i]];
- break;
+ if (rootId) {
+ if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
+ } else {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
}
}
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/emoticons.jsx b/web/react/utils/emoticons.jsx
index 94bb91503..7b43e48b4 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -3,6 +3,7 @@
const emoticonPatterns = {
smile: /(^|\s)(:-?\))($|\s)/g, // :)
+ wink: /(^|\s)(;-?\))($|\s)/g, // ;)
open_mouth: /(^|\s)(:o)($|\s)/gi, // :o
scream: /(^|\s)(:-o)($|\s)/gi, // :-o
smirk: /(^|\s)(:-?])($|\s)/g, // :]
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 561c2c4c4..b9084b26e 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -13,7 +13,8 @@ var client = require('./client.jsx');
var Autolinker = require('autolinker');
export function isEmail(email) {
- var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ //var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ var regex = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;
return regex.test(email);
}
@@ -518,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);
@@ -543,6 +544,7 @@ export function applyTheme(theme) {
if (theme.buttonBg) {
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
}
if (theme.buttonColor) {
@@ -967,3 +969,11 @@ export function getShortenedTeamURL() {
}
return teamURL + '/';
}
+
+export function windowWidth() {
+ return $(window).width();
+}
+
+export function windowHeight() {
+ return $(window).height();
+}
diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss
index f1aa4dca2..184fb55eb 100644
--- a/web/sass-files/sass/partials/_command-box.scss
+++ b/web/sass-files/sass/partials/_command-box.scss
@@ -5,6 +5,7 @@
border: $border-gray;
bottom: 38px;
overflow: auto;
+ z-index: 100;
@extend %popover-box-shadow;
.sidebar--right & {
bottom: 100px;
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 01057423d..d3ab3b9f8 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -133,12 +133,34 @@
height: 100%;
background-color: #FFF;
background-repeat: no-repeat;
+ overflow: hidden;
+ position: relative;
+ text-align: center;
&.small {
background-position: center;
}
&.normal {
background-position: top left;
}
+ .spinner.file__loading {
+ position: absolute;
+ left: 50%;
+ margin-left: -16px;
+ top: 50%;
+ margin-top: -16px;
+ }
+ .file__loaded {
+ max-width: initial;
+ &.landscape, &.quadrat {
+ height: 100px;
+ }
+ &.portrait {
+ width: 120px;
+ }
+ }
+ &:hover .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
.post-image__thumbnail {
float: left;
@@ -215,3 +237,20 @@
}
}
}
+
+.file-playback-controls {
+ position: absolute;
+ right: 5px;
+ bottom: 0;
+ font-size: 22px;
+ cursor: pointer;
+ z-index: 2;
+ -webkit-transition: opacity 0.6s;
+ -moz-transition: opacity 0.6s;
+ -o-transition: opacity 0.6s;
+ transition: opacity 0.6s;
+
+ &.stop {
+ @include opacity(0);
+ }
+}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index b942a5a40..1dcdbf348 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -140,7 +140,7 @@
padding: 0;
}
}
- .more-channel-table {
+ .more-table {
margin: 0;
table-layout: fixed;
p {
@@ -150,9 +150,11 @@
@include opacity(0.8);
margin: 5px 0;
}
- .more-channel-name {
+ .more-name {
font-weight: 600;
font-size: 0.95em;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
tbody {
> tr {
@@ -175,6 +177,9 @@
padding: 8px 15px 8px 8px;
width: 80px;
vertical-align: middle;
+ &.lg {
+ width: 110px;
+ }
}
}
}
@@ -189,7 +194,7 @@
position: relative;
max-width: 90%;
min-height: 100px;
- min-width: 240px;
+ min-width: 320px;
@include border-radius(3px);
display: table;
margin: 0 auto;
@@ -223,11 +228,24 @@
background: #FFF;
display: table-cell;
vertical-align: middle;
+ position: relative;
+
+ &:hover .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
img {
max-width: 100%;
max-height: 100%;
}
+ .spinner.file__loading {
+ z-index: 2;
+ position: absolute;
+ left: 50%;
+ margin-left: -16px;
+ top: 50%;
+ margin-top: -16px;
+ }
}
.modal-content{
box-shadow: none;
@@ -331,47 +349,42 @@
}
.modal-direct-channels {
- .user-list {
- list-style-type: none;
- margin: 15px 0px 0px;
- max-height: 600px;
- padding: 0px;
- overflow: auto;
- li {
- border-bottom: 1px solid #ddd;
- height: 60px;
- padding: 10px 0px;
+ .user-list {
+ margin-top: 10px;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ max-height: 500px;
+ position: relative;
+ }
- .image-div {
- padding: 0px;
+ .table {
+ margin-top: 10px;
+ }
- .profile-image {
- width: 40px;
- height: 40px;
- @include border-radius(20px);
- }
- }
+ .modal-body {
+ padding: 20px 0 0;
+ @include clearfix;
+ }
- .username {
- font-weight: bold;
- }
+ .filter-row {
+ padding: 0 15px;
+ }
- .nickname {
- color: #888;
- }
+ .member-count {
+ margin-top: 5px;
+ float: right;
+ @include opacity(0.8);
+ }
- .btn-div {
- padding: 0px;
- .btn-message {
- position: relative;
- top: 5px;
- }
- }
+ .more-description {
+ @include opacity(0.7);
+ }
- &:last-child {
- border-bottom: 0px;
- }
- }
- }
+ .profile-img {
+ -moz-border-radius: 50px;
+ -webkit-border-radius: 50px;
+ border-radius: 50px;
+ margin-right: 8px;
+ }
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 0f3cc0ef6..6ecc0d965 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -119,20 +119,52 @@ body.ios {
background-color: rgba(0, 0, 0, 0.6);
text-align: center;
color: #FFF;
- display: table;
- font-size: 1.7em;
+ font-size: em(20px);
font-weight: 600;
z-index: 6;
- > div {
- display: table-cell;
- vertical-align: middle;
+ &.right-file-overlay {
+ font-size: em(18px);
+ .overlay__circle {
+ width: 300px;
+ height: 300px;
+ margin: -150px 0 0 -150px;
+ }
+ .overlay__files {
+ margin: 60px auto 15px;
+ width: 150px;
+ }
}
- .fa {
+ .overlay__circle {
+ background: #111;
+ background: rgba(black, 0.7);
+ width: 370px;
+ height: 370px;
+ margin: -185px 0 0 -185px;
+ @include border-radius(500px);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ }
+
+ .overlay__files {
display: block;
- font-size: 2em;
- margin: 0 0 0.3em;
+ margin: 75px auto 20px;
+ }
+
+ .overlay__logo {
+ position: absolute;
+ left: 50%;
+ bottom: 30px;
+ margin-left: -50px;
+ @include opacity(0.3);
+ }
+
+ .fa {
+ display: inline-block;
+ font-size: 1.1em;
+ margin-right: 8px;
}
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 09f2c179e..c8bb24f3a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -199,9 +199,6 @@
}
@media screen and (max-width: 960px) {
- .center-file-overlay {
- font-size: 1.5em;
- }
.post {
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@@ -267,11 +264,28 @@
}
}
.file-details {
+ width: 100%;
height: auto;
}
}
- .center-file-overlay {
- font-size: 1.3em;
+ .modal-direct-channels {
+ .member-count {
+ float: none;
+ margin-top: 10px;
+ display: block;
+ }
+ }
+ .file-overlay {
+ font-size: em(18px);
+ .overlay__circle {
+ width: 300px;
+ height: 300px;
+ margin: -150px 0 0 -150px;
+ }
+ .overlay__files {
+ margin: 60px auto 15px;
+ width: 150px;
+ }
}
.date-separator, .new-separator {
&.hovered--after {
@@ -631,6 +645,9 @@
}
&.has-close {
.btn-close {
+ width: 40px;
+ text-align: center;
+ right: 0;
display: block;
@include opacity(0.5);
}
@@ -690,7 +707,7 @@
.modal-image {
.image-wrapper {
font-size: 12px;
- min-width: 280px;
+ min-width: 250px;
.modal-close {
@include opacity(1);
}
@@ -741,6 +758,10 @@
.post-comments {
padding: 9px 21px 10px 10px !important;
}
+
+ .post-image__column .post__image .file-playback-controls.stop, .image-wrapper > a .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
@media screen and (max-width: 640px) {
.access-history__table {
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 0c2f25eab..bc53dc0e4 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -103,6 +103,9 @@
text-overflow: ellipsis;
margin-bottom: 0;
}
+ .input-group-addon {
+ background: transparent;
+ }
.radio {
label {
font-weight: 600;
@@ -230,13 +233,6 @@
font-weight:500;
}
-.profile-img {
- width:128px;
- height:128px;
- margin-bottom: 10px;
- @include border-radius(128px);
-}
-
.sel-btn {
margin-right:5px;
}
@@ -298,3 +294,7 @@
.color-btn {
margin:4px;
}
+
+.no-resize {
+ resize:none;
+}
diff --git a/web/sass-files/sass/partials/_videos.scss b/web/sass-files/sass/partials/_videos.scss
index 9e1ce29b7..6ae5b488b 100644
--- a/web/sass-files/sass/partials/_videos.scss
+++ b/web/sass-files/sass/partials/_videos.scss
@@ -26,11 +26,6 @@
padding:0px;
}
-.video-uploader {
- font-size: 13px;
- margin: 0 0 15px;
-}
-
.video-title {
font-size:15px;
margin-top:3px;
@@ -54,4 +49,4 @@
border-top:36px solid transparent;
border-bottom:36px solid transparent;
border-left:60px solid rgba(255,255,255,0.4);
-} \ No newline at end of file
+}
diff --git a/web/static/images/filesOverlay.png b/web/static/images/filesOverlay.png
new file mode 100644
index 000000000..d24da7626
--- /dev/null
+++ b/web/static/images/filesOverlay.png
Binary files differ
diff --git a/web/static/images/logoWhite.png b/web/static/images/logoWhite.png
new file mode 100644
index 000000000..11bbd4632
--- /dev/null
+++ b/web/static/images/logoWhite.png
Binary files differ
diff --git a/web/static/images/webhook_icon.jpg b/web/static/images/webhook_icon.jpg
new file mode 100644
index 000000000..af5303421
--- /dev/null
+++ b/web/static/images/webhook_icon.jpg
Binary files differ
diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html
index a046478f6..574caf730 100644
--- a/web/templates/admin_console.html
+++ b/web/templates/admin_console.html
@@ -12,7 +12,7 @@
<div id='select_team_modal'></div>
<script>
- window.setup_admin_console_page();
+ window.setup_admin_console_page({{ .Props }});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
diff --git a/web/web.go b/web/web.go
index 7ab50a073..3bfed371b 100644
--- a/web/web.go
+++ b/web/web.go
@@ -15,7 +15,6 @@ import (
"gopkg.in/fsnotify.v1"
"html/template"
"net/http"
- "regexp"
"strconv"
"strings"
)
@@ -64,6 +63,9 @@ func InitWeb() {
mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET")
mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
+ mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET")
+ mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET")
+ mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET")
mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST")
@@ -427,9 +429,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
user := result.Data.(*model.User)
if user.LastActivityAt > 0 {
- api.FireAndForgetEmailChangeVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
} else {
- api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
@@ -699,7 +701,14 @@ func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
+ params := mux.Vars(r)
+ activeTab := params["tab"]
+ teamId := params["team"]
+
page := NewHtmlTemplatePage("admin_console", "Admin Console")
+
+ page.Props["ActiveTab"] = activeTab
+ page.Props["TeamId"] = teamId
page.Render(c, w)
}
@@ -921,9 +930,6 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"]
- overrideUsername := props["username"]
- overrideIconUrl := props["icon_url"]
-
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message)
@@ -952,12 +958,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
}
- // parse links into Markdown format
- linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
- text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
-
- linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
- text = linkRegex.ReplaceAllString(text, "${1}")
+ overrideUsername := props["username"]
+ overrideIconUrl := props["icon_url"]
if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)
@@ -968,27 +970,16 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
- post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
- post.AddProp("from_webhook", "true")
-
- if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
- post.AddProp("override_username", overrideUsername)
- }
-
- if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
- post.AddProp("override_icon_url", overrideIconUrl)
- }
+ // create a mock session
+ c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")
return
}
- // create a mock session
- c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
-
- if _, err := api.CreatePost(c, post, false); err != nil {
- c.Err = model.NewAppError("incomingWebhook", "Error creating post", "err="+err.Message)
+ if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl); err != nil {
+ c.Err = err
return
}