summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/admin_console/user_item.jsx90
-rw-r--r--web/react/components/center_panel.jsx4
-rw-r--r--web/react/components/channel_invite_modal.jsx21
-rw-r--r--web/react/components/channel_members_modal.jsx35
-rw-r--r--web/react/components/create_post.jsx4
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx9
-rw-r--r--web/react/components/member_list.jsx2
-rw-r--r--web/react/components/member_list_team_item.jsx70
-rw-r--r--web/react/components/post_attachment.jsx295
-rw-r--r--web/react/components/post_attachment_list.jsx32
-rw-r--r--web/react/components/post_body.jsx4
-rw-r--r--web/react/components/post_body_additional_content.jsx56
-rw-r--r--web/react/components/search_autocomplete.jsx157
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/sidebar_header.jsx2
-rw-r--r--web/react/components/team_members.jsx10
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx90
-rw-r--r--web/react/stores/browser_store.jsx37
-rw-r--r--web/react/stores/user_store.jsx9
-rw-r--r--web/react/utils/client.jsx1
-rw-r--r--web/react/utils/highlight.jsx51
-rw-r--r--web/react/utils/markdown.jsx351
-rw-r--r--web/react/utils/utils.jsx20
25 files changed, 1111 insertions, 253 deletions
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index f7e92672d..2badaf0e5 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -212,50 +212,52 @@ export default class UserItem extends React.Component {
}
return (
- <div className='row member-div'>
- <img
- className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
- height='36'
- width='36'
- />
- <span className='member-name'>{Utils.getDisplayName(user)}</span>
- <span className='member-email'>{email}</span>
- <div className='dropdown member-drop'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span>{currentRoles} </span>
- <span className='caret'></span>
- </a>
- <ul
- className='dropdown-menu member-menu'
- role='menu'
- aria-labelledby='channel_header_dropdown'
- >
- {makeAdmin}
- {makeMember}
- {makeActive}
- {makeNotActive}
- {makeSystemAdmin}
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.handleResetPassword}
- >
- {'Reset Password'}
- </a>
- </li>
- </ul>
- </div>
- {serverError}
- </div>
+ <tr>
+ <td className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </td>
+ </tr>
);
}
}
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index ea0eec747..3ee40bb86 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -21,7 +21,7 @@ export default class CenterPanel extends React.Component {
this.onPreferenceChange = this.onPreferenceChange.bind(this);
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
}
componentDidMount() {
@@ -31,7 +31,7 @@ export default class CenterPanel extends React.Component {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
onPreferenceChange() {
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
}
render() {
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 2dc12c9aa..7c1032321 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -54,6 +54,16 @@ export default class ChannelInviteModal extends React.Component {
loading
};
}
+ onShow() {
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ }
+ }
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
@@ -103,6 +113,11 @@ export default class ChannelInviteModal extends React.Component {
);
}
render() {
+ var maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
+ }
+
var inviteError = null;
if (this.state.inviteError) {
inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>);
@@ -129,13 +144,17 @@ export default class ChannelInviteModal extends React.Component {
return (
<Modal
+ dialogClassName='more-modal'
show={this.props.show}
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title>
</Modal.Header>
- <Modal.Body>
+ <Modal.Body
+ ref='modalBody'
+ style={{maxHeight}}
+ >
{inviteError}
{content}
</Modal.Body>
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 1df0da9cf..2fa7ae8ff 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -70,6 +70,16 @@ export default class ChannelMembersModal extends React.Component {
channelName
};
}
+ onShow() {
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ }
+ }
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
@@ -128,6 +138,11 @@ export default class ChannelMembersModal extends React.Component {
);
}
render() {
+ var maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
+ }
+
const currentMember = ChannelStore.getCurrentMember();
let isAdmin = false;
if (currentMember) {
@@ -137,6 +152,7 @@ export default class ChannelMembersModal extends React.Component {
return (
<div>
<Modal
+ dialogClassName='more-modal'
show={this.props.show}
onHide={this.props.onModalDismissed}
>
@@ -153,15 +169,16 @@ export default class ChannelMembersModal extends React.Component {
<i className='glyphicon glyphicon-envelope'/>{' Add New Members'}
</a>
</Modal.Header>
- <Modal.Body>
- <div className='col-sm-12'>
- <div className='team-member-list'>
- <MemberList
- memberList={this.state.memberList}
- isAdmin={isAdmin}
- handleRemove={this.handleRemove}
- />
- </div>
+ <Modal.Body
+ ref='modalBody'
+ style={{maxHeight}}
+ >
+ <div className='team-member-list'>
+ <MemberList
+ memberList={this.state.memberList}
+ isAdmin={isAdmin}
+ handleRemove={this.handleRemove}
+ />
</div>
</Modal.Body>
<Modal.Footer>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 4d1874e18..5a69c9bfb 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -50,7 +50,7 @@ export default class CreatePost extends React.Component {
PostStore.clearDraftUploads();
const draft = this.getCurrentDraft();
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.state = {
channelId: ChannelStore.getCurrentId(),
@@ -336,7 +336,7 @@ export default class CreatePost extends React.Component {
}
}
onPreferenceChange() {
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({
showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER,
ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
index 4cb96a3ff..65e8183de 100644
--- a/web/react/components/edit_channel_purpose_modal.jsx
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -3,6 +3,8 @@
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
+const Utils = require('../utils/utils.jsx');
+
const Modal = ReactBootstrap.Modal;
export default class EditChannelPurposeModal extends React.Component {
@@ -75,11 +77,6 @@ export default class EditChannelPurposeModal extends React.Component {
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
}
- let channelTerm = 'Channel';
- if (this.props.channel.channelType === 'P') {
- channelTerm = 'Group';
- }
-
return (
<Modal
className='modal-edit-channel-purpose'
@@ -93,7 +90,7 @@ export default class EditChannelPurposeModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <p>{`Describe how this ${channelTerm} should be used.`}</p>
+ <p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used.`}</p>
<textarea
ref='purpose'
className='form-control no-resize'
diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx
index 9c0243291..0238c7920 100644
--- a/web/react/components/member_list.jsx
+++ b/web/react/components/member_list.jsx
@@ -15,7 +15,7 @@ export default class MemberList extends React.Component {
members = this.props.memberList;
}
- var message = '';
+ var message = null;
if (members.length === 0) {
message = <tr><td>No users to add.</td></tr>;
}
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 14db05cdb..1fa369068 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -166,40 +166,42 @@ export default class MemberListTeamItem extends React.Component {
}
return (
- <div className='row member-div'>
- <img
- className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`}
- height='36'
- width='36'
- />
- <span className='member-name'>{Utils.getDisplayName(user)}</span>
- <span className='member-email'>{email}</span>
- <div className='dropdown member-drop'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span>{currentRoles} </span>
- <span className='caret'></span>
- </a>
- <ul
- className='dropdown-menu member-menu'
- role='menu'
- aria-labelledby='channel_header_dropdown'
- >
- {makeAdmin}
- {makeMember}
- {makeActive}
- {makeNotActive}
- </ul>
- </div>
- {serverError}
- </div>
+ <tr>
+ <td className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ </ul>
+ </div>
+ {serverError}
+ </td>
+ </tr>
);
}
}
diff --git a/web/react/components/post_attachment.jsx b/web/react/components/post_attachment.jsx
new file mode 100644
index 000000000..2d6b47f03
--- /dev/null
+++ b/web/react/components/post_attachment.jsx
@@ -0,0 +1,295 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const TextFormatting = require('../utils/text_formatting.jsx');
+
+export default class PostAttachment extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getFieldsTable = this.getFieldsTable.bind(this);
+ this.getInitState = this.getInitState.bind(this);
+ this.shouldCollapse = this.shouldCollapse.bind(this);
+ this.toggleCollapseState = this.toggleCollapseState.bind(this);
+ }
+
+ componentDidMount() {
+ $(this.refs.attachment).on('click', '.attachment-link-more', this.toggleCollapseState);
+ }
+
+ componentWillUnmount() {
+ $(this.refs.attachment).off('click', '.attachment-link-more', this.toggleCollapseState);
+ }
+
+ componentWillMount() {
+ this.setState(this.getInitState());
+ }
+
+ getInitState() {
+ const shouldCollapse = this.shouldCollapse();
+ const text = TextFormatting.formatText(this.props.attachment.text || '');
+ const uncollapsedText = text + (shouldCollapse ? '<a class="attachment-link-more" href="#">▲ collapse text</a>' : '');
+ const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
+
+ return {
+ shouldCollapse,
+ collapsedText,
+ uncollapsedText,
+ text: shouldCollapse ? collapsedText : uncollapsedText,
+ collapsed: shouldCollapse
+ };
+ }
+
+ toggleCollapseState(e) {
+ e.preventDefault();
+
+ let state = this.state;
+ state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
+ state.collapsed = !state.collapsed;
+ this.setState(state);
+ }
+
+ shouldCollapse() {
+ return (this.props.attachment.text.match(/\n/g) || []).length >= 5 || this.props.attachment.text.length > 700;
+ }
+
+ getCollapsedText() {
+ let text = this.props.attachment.text || '';
+ if ((text.match(/\n/g) || []).length >= 5) {
+ text = text.split('\n').splice(0, 5).join('\n');
+ } else if (text.length > 700) {
+ text = text.substr(0, 700);
+ }
+
+ return TextFormatting.formatText(text) + '<a class="attachment-link-more" href="#">▼ read more</a>';
+ }
+
+ getFieldsTable() {
+ const fields = this.props.attachment.fields;
+ if (!fields || !fields.length) {
+ return '';
+ }
+
+ const compactTable = fields.filter((field) => field.short).length > 0;
+ let tHead;
+ let tBody;
+
+ if (compactTable) {
+ let headerCols = [];
+ let bodyCols = [];
+
+ fields.forEach((field, i) => {
+ headerCols.push(
+ <th
+ className='attachment___field-caption'
+ key={'attachment__field-caption-' + i}
+ >
+ {field.title}
+ </th>
+ );
+ bodyCols.push(
+ <td
+ className='attachment___field'
+ key={'attachment__field-' + i}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
+ >
+ </td>
+ );
+ });
+
+ tHead = (
+ <tr>
+ {headerCols}
+ </tr>
+ );
+ tBody = (
+ <tr>
+ {bodyCols}
+ </tr>
+ );
+ } else {
+ tBody = [];
+
+ fields.forEach((field, i) => {
+ tBody.push(
+ <tr key={'attachment__field-' + i}>
+ <td
+ className='attachment___field-caption'
+ >
+ {field.title}
+ </td>
+ <td
+ className='attachment___field'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
+ >
+ </td>
+ </tr>
+ );
+ });
+ }
+
+ return (
+ <table
+ className='attachment___fields'
+ >
+ <thead>
+ {tHead}
+ </thead>
+ <tbody>
+ {tBody}
+ </tbody>
+ </table>
+ );
+ }
+
+ render() {
+ const data = this.props.attachment;
+
+ let preText;
+ if (data.pretext) {
+ preText = (
+ <div
+ className='attachment__thumb-pretext'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(data.pretext)}}
+ >
+ </div>
+ );
+ }
+
+ let author = [];
+ if (data.author_name || data.author_icon) {
+ if (data.author_icon) {
+ author.push(
+ <img
+ className='attachment__author-icon'
+ src={data.author_icon}
+ key={'attachment__author-icon'}
+ height='14'
+ width='14'
+ />
+ );
+ }
+ if (data.author_name) {
+ author.push(
+ <span
+ className='attachment__author-name'
+ key={'attachment__author-name'}
+ >
+ {data.author_name}
+ </span>
+ );
+ }
+ }
+ if (data.author_link) {
+ author = (
+ <a
+ href={data.author_link}
+ target='_blank'
+ >
+ {author}
+ </a>
+ );
+ }
+
+ let title;
+ if (data.title) {
+ if (data.title_link) {
+ title = (
+ <h1
+ className='attachment__title'
+ >
+ <a
+ className='attachment__title-link'
+ href={data.title_link}
+ target='_blank'
+ >
+ {data.title}
+ </a>
+ </h1>
+ );
+ } else {
+ title = (
+ <h1
+ className='attachment__title'
+ >
+ {data.title}
+ </h1>
+ );
+ }
+ }
+
+ let text;
+ if (data.text) {
+ text = (
+ <div
+ className='attachment__text'
+ dangerouslySetInnerHTML={{__html: this.state.text}}
+ >
+ </div>
+ );
+ }
+
+ let image;
+ if (data.image_url) {
+ image = (
+ <img
+ className='attachment__image'
+ src={data.image_url}
+ />
+ );
+ }
+
+ let thumb;
+ if (data.thumb_url) {
+ thumb = (
+ <div
+ className='attachment__thumb-container'
+ >
+ <img
+ src={data.thumb_url}
+ />
+ </div>
+ );
+ }
+
+ const fields = this.getFieldsTable();
+
+ let useBorderStyle;
+ if (data.color && data.color[0] === '#') {
+ useBorderStyle = {borderLeftColor: data.color};
+ }
+
+ return (
+ <div
+ className='attachment'
+ ref='attachment'
+ >
+ {preText}
+ <div className='attachment__content'>
+ <div
+ className={useBorderStyle ? 'clearfix attachment__container' : 'clearfix attachment__container attachment__container--' + data.color}
+ style={useBorderStyle}
+ >
+ {author}
+ {title}
+ <div>
+ <div
+ className={thumb ? 'attachment__body' : 'attachment__body attachment__body--no_thumb'}
+ >
+ {text}
+ {image}
+ {fields}
+ </div>
+ {thumb}
+ <div style={{clear: 'both'}}></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+PostAttachment.propTypes = {
+ attachment: React.PropTypes.object.isRequired
+}; \ No newline at end of file
diff --git a/web/react/components/post_attachment_list.jsx b/web/react/components/post_attachment_list.jsx
new file mode 100644
index 000000000..03b866656
--- /dev/null
+++ b/web/react/components/post_attachment_list.jsx
@@ -0,0 +1,32 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const PostAttachment = require('./post_attachment.jsx');
+
+export default class PostAttachmentList extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ let content = [];
+ this.props.attachments.forEach((attachment, i) => {
+ content.push(
+ <PostAttachment
+ attachment={attachment}
+ key={'att_' + i}
+ />
+ );
+ });
+
+ return (
+ <div className='attachment_list'>
+ {content}
+ </div>
+ );
+ }
+}
+
+PostAttachmentList.propTypes = {
+ attachments: React.PropTypes.array.isRequired
+};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index e4094daf3..5a157b792 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -7,6 +7,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const TextFormatting = require('../utils/text_formatting.jsx');
const twemoji = require('twemoji');
+const PostBodyAdditionalContent = require('./post_body_additional_content.jsx');
export default class PostBody extends React.Component {
constructor(props) {
@@ -331,6 +332,9 @@ export default class PostBody extends React.Component {
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
</div>
+ <PostBodyAdditionalContent
+ post={post}
+ />
{fileAttachmentHolder}
{embed}
</div>
diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx
new file mode 100644
index 000000000..8189ba2d3
--- /dev/null
+++ b/web/react/components/post_body_additional_content.jsx
@@ -0,0 +1,56 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const PostAttachmentList = require('./post_attachment_list.jsx');
+
+export default class PostBodyAdditionalContent extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getSlackAttachment = this.getSlackAttachment.bind(this);
+ this.getComponent = this.getComponent.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({type: this.props.post.type, shouldRender: Boolean(this.props.post.type)});
+ }
+
+ getSlackAttachment() {
+ const attachments = this.props.post.props && this.props.post.props.attachments || [];
+ return (
+ <PostAttachmentList
+ key={'post_body_additional_content' + this.props.post.id}
+ attachments={attachments}
+ />
+ );
+ }
+
+ getComponent() {
+ switch (this.state.type) {
+ case 'slack_attachment':
+ return this.getSlackAttachment();
+ }
+ }
+
+ render() {
+ let content = [];
+
+ if (this.state.shouldRender) {
+ const component = this.getComponent();
+
+ if (component) {
+ content = component;
+ }
+ }
+
+ return (
+ <div>
+ {content}
+ </div>
+ );
+ }
+}
+
+PostBodyAdditionalContent.propTypes = {
+ post: React.PropTypes.object.isRequired
+}; \ No newline at end of file
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03e14ec49..736919697 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -3,14 +3,15 @@
const ChannelStore = require('../stores/channel_store.jsx');
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
+const Popover = ReactBootstrap.Popover;
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
-const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -22,8 +23,13 @@ export default class SearchAutocomplete extends React.Component {
this.handleKeyDown = this.handleKeyDown.bind(this);
this.completeWord = this.completeWord.bind(this);
+ this.getSelection = this.getSelection.bind(this);
+ this.scrollToItem = this.scrollToItem.bind(this);
this.updateSuggestions = this.updateSuggestions.bind(this);
+ this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this);
+ this.renderUserSuggestion = this.renderUserSuggestion.bind(this);
+
this.state = {
show: false,
mode: '',
@@ -37,9 +43,18 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
- componentDidUpdate() {
- $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
- $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ componentDidUpdate(prevProps, prevState) {
+ const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
+
+ if (this.state.show) {
+ if (!prevState.show) {
+ content.perfectScrollbar();
+ content.css('max-height', $(window).height() - 200);
+ }
+
+ // keep the keyboard selection visible when scrolling
+ this.scrollToItem(this.getSelection());
+ }
}
componentWillUnmount() {
@@ -51,7 +66,7 @@ export default class SearchAutocomplete extends React.Component {
}
handleDocumentClick(e) {
- const container = $(ReactDOM.findDOMNode(this.refs.container));
+ const container = $(ReactDOM.findDOMNode(this.refs.searchPopover));
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
this.setState({
@@ -111,15 +126,7 @@ export default class SearchAutocomplete extends React.Component {
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
e.preventDefault();
- this.completeSelectedWord();
- }
- }
-
- completeSelectedWord() {
- if (this.state.mode === 'channels') {
- this.completeWord(this.state.suggestions[this.state.selection].name);
- } else if (this.state.mode === 'users') {
- this.completeWord(this.state.suggestions[this.state.selection].username);
+ this.completeWord(this.getSelection());
}
}
@@ -135,6 +142,40 @@ export default class SearchAutocomplete extends React.Component {
});
}
+ getSelection() {
+ if (this.state.mode === 'channels') {
+ return this.state.suggestions[this.state.selection].name;
+ } else if (this.state.mode === 'users') {
+ return this.state.suggestions[this.state.selection].username;
+ }
+
+ return '';
+ }
+
+ scrollToItem(itemName) {
+ const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
+ const visibleContentHeight = content[0].clientHeight;
+ const actualContentHeight = content[0].scrollHeight;
+
+ if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) {
+ const contentTop = content.scrollTop();
+ const contentTopPadding = parseInt(content.css('padding-top'), 10);
+ const contentBottomPadding = parseInt(content.css('padding-top'), 10);
+
+ const item = $(this.refs[itemName]);
+ const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
+ const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
+
+ if (itemTop - contentTopPadding < contentTop) {
+ // the item is off the top of the visible space
+ content.scrollTop(itemTop - contentTopPadding);
+ } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
+ // the item has gone off the bottom of the visible space
+ content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ }
+ }
+ }
+
updateSuggestions(mode, filter) {
let suggestions = [];
@@ -193,6 +234,46 @@ export default class SearchAutocomplete extends React.Component {
});
}
+ renderChannelSuggestion(channel) {
+ let className = 'search-autocomplete__item';
+ if (channel.name === this.getSelection()) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={channel.name}
+ ref={channel.name}
+ onClick={this.handleClick.bind(this, channel.name)}
+ className={className}
+ >
+ {channel.name}
+ </div>
+ );
+ }
+
+ renderUserSuggestion(user) {
+ let className = 'search-autocomplete__item';
+ if (user.username === this.getSelection()) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={user.username}
+ ref={user.username}
+ onClick={this.handleClick.bind(this, user.username)}
+ className={className}
+ >
+ <img
+ className='profile-img rounded'
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
+ />
+ {user.username}
+ </div>
+ );
+ }
+
render() {
if (!this.state.show || this.state.suggestions.length === 0) {
return null;
@@ -201,45 +282,33 @@ export default class SearchAutocomplete extends React.Component {
let suggestions = [];
if (this.state.mode === 'channels') {
- suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__item';
- if (this.state.selection === index) {
- className += ' selected';
- }
-
- return (
+ const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
+ if (publicChannels.length > 0) {
+ suggestions.push(
<div
- key={channel.name}
- ref={channel.name}
- onClick={this.handleClick.bind(this, channel.name)}
- className={className}
+ key='public-channel-divider'
+ className='search-autocomplete__divider'
>
- {channel.name}
+ {'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}
</div>
);
- });
- } else if (this.state.mode === 'users') {
- suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__item';
- if (this.state.selection === index) {
- className += ' selected';
- }
+ suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion));
+ }
- return (
+ const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
+ if (privateChannels.length > 0) {
+ suggestions.push(
<div
- key={user.username}
- ref={user.username}
- onClick={this.handleClick.bind(this, user.username)}
- className={className}
+ key='private-channel-divider'
+ className='search-autocomplete__divider'
>
- <img
- className='profile-img rounded'
- src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
- />
- {user.username}
+ {'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}
</div>
);
- });
+ suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion));
+ }
+ } else if (this.state.mode === 'users') {
+ suggestions = this.state.suggestions.map(this.renderUserSuggestion);
}
return (
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 8b5f7a381..f5ce5c10e 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -146,7 +146,7 @@ export default class Sidebar extends React.Component {
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
return {
activeId: currentChannelId,
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 46730e1e6..bc7f6ba50 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -31,7 +31,7 @@ export default class SidebarHeader extends React.Component {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
getStateFromStores() {
- const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER};
}
diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx
index 33590c89a..ac1ebf52d 100644
--- a/web/react/components/team_members.jsx
+++ b/web/react/components/team_members.jsx
@@ -79,7 +79,7 @@ export default class TeamMembers extends React.Component {
return (
<div
- className='modal fade'
+ className='modal fade more-modal'
ref='modal'
id='team_members'
tabIndex='-1'
@@ -106,12 +106,10 @@ export default class TeamMembers extends React.Component {
ref='modalBody'
className='modal-body'
>
- <div className='channel-settings'>
- <div className='team-member-list'>
- {renderMembers}
- </div>
- {serverError}
+ <div className='team-member-list'>
+ {renderMembers}
</div>
+ {serverError}
</div>
<div className='modal-footer'>
<button
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 24da106d0..4d594bb1b 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -50,7 +50,7 @@ export default class ImportThemeModal extends React.Component {
theme.sidebarText = colors[5];
theme.sidebarUnreadText = colors[5];
theme.sidebarTextHoverBg = colors[4];
- theme.sidebarTextActiveBg = colors[2];
+ theme.sidebarTextActiveBorder = colors[2];
theme.sidebarTextActiveColor = colors[3];
theme.sidebarHeaderBg = colors[1];
theme.sidebarHeaderTextColor = colors[5];
@@ -59,9 +59,13 @@ export default class ImportThemeModal extends React.Component {
theme.mentionColor = '#ffffff';
theme.centerChannelBg = '#ffffff';
theme.centerChannelColor = '#333333';
+ theme.newMessageSeparator = '#F80';
theme.linkColor = '#2389d7';
theme.buttonBg = '#26a970';
theme.buttonColor = '#ffffff';
+ theme.mentionHighlightBg = '#fff2bb';
+ theme.mentionHighlightLink = '#2f81b7';
+ theme.codeTheme = 'github';
let user = UserStore.getCurrentUser();
user.theme_props = theme;
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 3c12ead23..43c8d33d1 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -172,13 +172,13 @@ export default class UserSettingsDisplay extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'How should other users be shown in Direct Messages list?'}</div>
+ <div><br/>{'Set what name to display in the Direct Messages list.'}</div>
</div>
];
nameFormatSection = (
<SettingItemMax
- title='Show real names, nick names or usernames?'
+ title='Teammate Name Display'
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -200,7 +200,7 @@ export default class UserSettingsDisplay extends React.Component {
nameFormatSection = (
<SettingItemMin
- title='Show real names, nick names or usernames?'
+ title='Teammate Name Display'
describe={describe}
updateSection={() => {
this.props.updateSection('name_format');
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 9f0c16194..1bfae6930 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -451,44 +451,60 @@ export default class UserSettingsGeneralTab extends React.Component {
}
}
- inputs.push(
- <div key='emailSetting'>
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{'Primary Email'}</label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- onChange={this.updateEmail}
- value={this.state.email}
- />
+ let submit = null;
+
+ if (this.props.user.auth_service === '') {
+ inputs.push(
+ <div key='emailSetting'>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{'Primary Email'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateEmail}
+ value={this.state.email}
+ />
+ </div>
</div>
</div>
- </div>
- );
-
- inputs.push(
- <div key='confirmEmailSetting'>
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{'Confirm Email'}</label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- onChange={this.updateConfirmEmail}
- value={this.state.confirmEmail}
- />
+ );
+
+ inputs.push(
+ <div key='confirmEmailSetting'>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{'Confirm Email'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateConfirmEmail}
+ value={this.state.confirmEmail}
+ />
+ </div>
</div>
+ {helpText}
</div>
- {helpText}
- </div>
- );
+ );
+
+ submit = this.submitEmail;
+ } else {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>{'Log in occurs through GitLab. Email cannot be updated.'}</div>
+ {helpText}
+ </div>
+ );
+ }
emailSection = (
<SettingItemMax
title='Email'
inputs={inputs}
- submit={this.submitEmail}
+ submit={submit}
server_error={serverError}
client_error={emailError}
updateSection={function clearSection(e) {
@@ -499,15 +515,19 @@ export default class UserSettingsGeneralTab extends React.Component {
);
} else {
let describe = '';
- if (this.state.emailChangeInProgress) {
- const newEmail = UserStore.getCurrentUser().email;
- if (newEmail) {
- describe = 'New Address: ' + newEmail + '\nCheck your email to verify the above address.';
+ if (this.props.user.auth_service === '') {
+ if (this.state.emailChangeInProgress) {
+ const newEmail = UserStore.getCurrentUser().email;
+ if (newEmail) {
+ describe = 'New Address: ' + newEmail + '\nCheck your email to verify the above address.';
+ } else {
+ describe = 'Check your email to verify your new address';
+ }
} else {
- describe = 'Check your email to verify your new address';
+ describe = UserStore.getCurrentUser().email;
}
} else {
- describe = UserStore.getCurrentUser().email;
+ describe = 'Log in done through GitLab';
}
emailSection = (
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index 75fb8aa3c..8e86ce32f 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -24,11 +24,17 @@ class BrowserStoreClass {
this.setLastServerVersion = this.setLastServerVersion.bind(this);
this.clear = this.clear.bind(this);
this.clearAll = this.clearAll.bind(this);
+ this.checkedLocalStorageSupported = '';
+ this.signalLogout = this.signalLogout.bind(this);
var currentVersion = sessionStorage.getItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
sessionStorage.clear();
- sessionStorage.setItem('storage_version', global.window.mm_config.Version);
+ try {
+ sessionStorage.setItem('storage_version', global.window.mm_config.Version);
+ } catch (e) {
+ // Do nothing
+ }
}
}
@@ -105,6 +111,13 @@ class BrowserStoreClass {
sessionStorage.setItem('last_server_version', version);
}
+ signalLogout() {
+ if (this.isLocalStorageSupported()) {
+ localStorage.setItem('__logout__', 'yes');
+ localStorage.removeItem('__logout__');
+ }
+ }
+
/**
* Preforms the given action on each item that has the given prefix
* Signature for action is action(key, value)
@@ -147,20 +160,26 @@ class BrowserStoreClass {
}
isLocalStorageSupported() {
+ if (this.checkedLocalStorageSupported !== '') {
+ return this.checkedLocalStorageSupported;
+ }
+
try {
- sessionStorage.setItem('testSession', '1');
- sessionStorage.removeItem('testSession');
+ sessionStorage.setItem('__testSession__', '1');
+ sessionStorage.removeItem('__testSession__');
- localStorage.setItem('testLocal', '1');
- if (localStorage.getItem('testLocal') !== '1') {
- return false;
+ localStorage.setItem('__testLocal__', '1');
+ if (localStorage.getItem('__testLocal__') !== '1') {
+ this.checkedLocalStorageSupported = false;
}
- localStorage.removeItem('testLocal', '1');
+ localStorage.removeItem('__testLocal__', '1');
- return true;
+ this.checkedLocalStorageSupported = true;
} catch (e) {
- return false;
+ this.checkedLocalStorageSupported = false;
}
+
+ return this.checkedLocalStorageSupported;
}
}
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 4fa7224b7..6b7d671fc 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -58,6 +58,8 @@ class UserStoreClass extends EventEmitter {
this.setStatus = this.setStatus.bind(this);
this.getStatuses = this.getStatuses.bind(this);
this.getStatus = this.getStatus.bind(this);
+
+ this.profileCache = null;
}
emitChange(userId) {
@@ -184,6 +186,10 @@ class UserStoreClass extends EventEmitter {
}
getProfiles() {
+ if (this.profileCache !== null) {
+ return this.profileCache;
+ }
+
return BrowserStore.getItem('profiles', {});
}
@@ -218,6 +224,7 @@ class UserStoreClass extends EventEmitter {
saveProfile(profile) {
var ps = this.getProfiles();
ps[profile.id] = profile;
+ this.profileCache = ps;
BrowserStore.setItem('profiles', ps);
}
@@ -226,6 +233,8 @@ class UserStoreClass extends EventEmitter {
if (currentId in profiles) {
delete profiles[currentId];
}
+
+ this.profileCache = profiles;
BrowserStore.setItem('profiles', profiles);
}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 003e24d33..d27fe16cf 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -231,6 +231,7 @@ export function resetPassword(data, success, error) {
export function logout() {
track('api', 'api_users_logout');
var currentTeamUrl = TeamStore.getCurrentTeamUrl();
+ BrowserStore.signalLogout();
BrowserStore.clear();
ErrorStore.storeLastError(null);
window.location.href = currentTeamUrl + '/logout';
diff --git a/web/react/utils/highlight.jsx b/web/react/utils/highlight.jsx
new file mode 100644
index 000000000..68fef7930
--- /dev/null
+++ b/web/react/utils/highlight.jsx
@@ -0,0 +1,51 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const highlightJs = require('highlight.js/lib/highlight.js');
+const highlightJsDiff = require('highlight.js/lib/languages/diff.js');
+const highlightJsApache = require('highlight.js/lib/languages/apache.js');
+const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js');
+const highlightJsHttp = require('highlight.js/lib/languages/http.js');
+const highlightJsJson = require('highlight.js/lib/languages/json.js');
+const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js');
+const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js');
+const highlightJsCss = require('highlight.js/lib/languages/css.js');
+const highlightJsNginx = require('highlight.js/lib/languages/nginx.js');
+const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js');
+const highlightJsPython = require('highlight.js/lib/languages/python.js');
+const highlightJsXml = require('highlight.js/lib/languages/xml.js');
+const highlightJsPerl = require('highlight.js/lib/languages/perl.js');
+const highlightJsBash = require('highlight.js/lib/languages/bash.js');
+const highlightJsPhp = require('highlight.js/lib/languages/php.js');
+const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js');
+const highlightJsCs = require('highlight.js/lib/languages/cs.js');
+const highlightJsCpp = require('highlight.js/lib/languages/cpp.js');
+const highlightJsSql = require('highlight.js/lib/languages/sql.js');
+const highlightJsGo = require('highlight.js/lib/languages/go.js');
+const highlightJsRuby = require('highlight.js/lib/languages/ruby.js');
+const highlightJsJava = require('highlight.js/lib/languages/java.js');
+const highlightJsIni = require('highlight.js/lib/languages/ini.js');
+
+highlightJs.registerLanguage('diff', highlightJsDiff);
+highlightJs.registerLanguage('apache', highlightJsApache);
+highlightJs.registerLanguage('makefile', highlightJsMakefile);
+highlightJs.registerLanguage('http', highlightJsHttp);
+highlightJs.registerLanguage('json', highlightJsJson);
+highlightJs.registerLanguage('markdown', highlightJsMarkdown);
+highlightJs.registerLanguage('javascript', highlightJsJavascript);
+highlightJs.registerLanguage('css', highlightJsCss);
+highlightJs.registerLanguage('nginx', highlightJsNginx);
+highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
+highlightJs.registerLanguage('python', highlightJsPython);
+highlightJs.registerLanguage('xml', highlightJsXml);
+highlightJs.registerLanguage('perl', highlightJsPerl);
+highlightJs.registerLanguage('bash', highlightJsBash);
+highlightJs.registerLanguage('php', highlightJsPhp);
+highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
+highlightJs.registerLanguage('cs', highlightJsCs);
+highlightJs.registerLanguage('cpp', highlightJsCpp);
+highlightJs.registerLanguage('sql', highlightJsSql);
+highlightJs.registerLanguage('go', highlightJsGo);
+highlightJs.registerLanguage('ruby', highlightJsRuby);
+highlightJs.registerLanguage('java', highlightJsJava);
+highlightJs.registerLanguage('ini', highlightJsIni);
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 3ef09211f..374caf6dc 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -1,38 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+require('./highlight.jsx');
const TextFormatting = require('./text_formatting.jsx');
const Utils = require('./utils.jsx');
+const highlightJs = require('highlight.js/lib/highlight.js');
const marked = require('marked');
-const highlightJs = require('highlight.js/lib/highlight.js');
-const highlightJsDiff = require('highlight.js/lib/languages/diff.js');
-const highlightJsApache = require('highlight.js/lib/languages/apache.js');
-const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js');
-const highlightJsHttp = require('highlight.js/lib/languages/http.js');
-const highlightJsJson = require('highlight.js/lib/languages/json.js');
-const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js');
-const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js');
-const highlightJsCss = require('highlight.js/lib/languages/css.js');
-const highlightJsNginx = require('highlight.js/lib/languages/nginx.js');
-const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js');
-const highlightJsPython = require('highlight.js/lib/languages/python.js');
-const highlightJsXml = require('highlight.js/lib/languages/xml.js');
-const highlightJsPerl = require('highlight.js/lib/languages/perl.js');
-const highlightJsBash = require('highlight.js/lib/languages/bash.js');
-const highlightJsPhp = require('highlight.js/lib/languages/php.js');
-const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js');
-const highlightJsCs = require('highlight.js/lib/languages/cs.js');
-const highlightJsCpp = require('highlight.js/lib/languages/cpp.js');
-const highlightJsSql = require('highlight.js/lib/languages/sql.js');
-const highlightJsGo = require('highlight.js/lib/languages/go.js');
-const highlightJsRuby = require('highlight.js/lib/languages/ruby.js');
-const highlightJsJava = require('highlight.js/lib/languages/java.js');
-const highlightJsIni = require('highlight.js/lib/languages/ini.js');
-
-const Constants = require('../utils/constants.jsx');
-const HighlightedLanguages = Constants.HighlightedLanguages;
+const HighlightedLanguages = require('../utils/constants.jsx').HighlightedLanguages;
function markdownImageLoaded(image) {
image.style.height = 'auto';
@@ -84,30 +60,6 @@ class MattermostMarkdownRenderer extends marked.Renderer {
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
-
- highlightJs.registerLanguage('diff', highlightJsDiff);
- highlightJs.registerLanguage('apache', highlightJsApache);
- highlightJs.registerLanguage('makefile', highlightJsMakefile);
- highlightJs.registerLanguage('http', highlightJsHttp);
- highlightJs.registerLanguage('json', highlightJsJson);
- highlightJs.registerLanguage('markdown', highlightJsMarkdown);
- highlightJs.registerLanguage('javascript', highlightJsJavascript);
- highlightJs.registerLanguage('css', highlightJsCss);
- highlightJs.registerLanguage('nginx', highlightJsNginx);
- highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
- highlightJs.registerLanguage('python', highlightJsPython);
- highlightJs.registerLanguage('xml', highlightJsXml);
- highlightJs.registerLanguage('perl', highlightJsPerl);
- highlightJs.registerLanguage('bash', highlightJsBash);
- highlightJs.registerLanguage('php', highlightJsPhp);
- highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
- highlightJs.registerLanguage('cs', highlightJsCs);
- highlightJs.registerLanguage('cpp', highlightJsCpp);
- highlightJs.registerLanguage('sql', highlightJsSql);
- highlightJs.registerLanguage('go', highlightJsGo);
- highlightJs.registerLanguage('ruby', highlightJsRuby);
- highlightJs.registerLanguage('java', highlightJsJava);
- highlightJs.registerLanguage('ini', highlightJsIni);
}
code(code, language) {
@@ -204,6 +156,301 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
}
+class MattermostLexer extends marked.Lexer {
+ token(originalSrc, top, bq) {
+ let src = originalSrc.replace(/^ +$/gm, '');
+
+ while (src) {
+ // newline
+ let cap = this.rules.newline.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ if (cap[0].length > 1) {
+ this.tokens.push({
+ type: 'space'
+ });
+ }
+ }
+
+ // code
+ cap = this.rules.code.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ cap = cap[0].replace(/^ {4}/gm, '');
+ this.tokens.push({
+ type: 'code',
+ text: this.options.pedantic ? cap : cap.replace(/\n+$/, '')
+ });
+ continue;
+ }
+
+ // fences (gfm)
+ cap = this.rules.fences.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'code',
+ lang: cap[2],
+ text: cap[3] || ''
+ });
+ continue;
+ }
+
+ // heading
+ cap = this.rules.heading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[1].length,
+ text: cap[2]
+ });
+ continue;
+ }
+
+ // table no leading pipe (gfm)
+ cap = this.rules.nptable.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // lheading
+ cap = this.rules.lheading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[2] === '=' ? 1 : 2,
+ text: cap[1]
+ });
+ continue;
+ }
+
+ // hr
+ cap = this.rules.hr.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'hr'
+ });
+ continue;
+ }
+
+ // blockquote
+ cap = this.rules.blockquote.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+
+ this.tokens.push({
+ type: 'blockquote_start'
+ });
+
+ cap = cap[0].replace(/^ *> ?/gm, '');
+
+ // Pass `top` to keep the current
+ // "toplevel" state. This is exactly
+ // how markdown.pl works.
+ this.token(cap, top, true);
+
+ this.tokens.push({
+ type: 'blockquote_end'
+ });
+
+ continue;
+ }
+
+ // list
+ cap = this.rules.list.exec(src);
+ if (cap) {
+ const bull = cap[2];
+ let l = cap[0].length;
+
+ // Get each top-level item.
+ cap = cap[0].match(this.rules.item);
+
+ if (cap.length > 1) {
+ src = src.substring(l);
+
+ this.tokens.push({
+ type: 'list_start',
+ ordered: bull.length > 1
+ });
+
+ let next = false;
+ l = cap.length;
+
+ for (let i = 0; i < l; i++) {
+ let item = cap[i];
+
+ // Remove the list item's bullet
+ // so it is seen as the next token.
+ let space = item.length;
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+ // Outdent whatever the
+ // list item contains. Hacky.
+ if (~item.indexOf('\n ')) {
+ space -= item.length;
+ item = this.options.pedantic ? item.replace(/^ {1,4}/gm, '') : item.replace(new RegExp('^ \{1,' + space + '\}', 'gm'), '');
+ }
+
+ // Determine whether the next list item belongs here.
+ // Backpedal if it does not belong in this list.
+ if (this.options.smartLists && i !== l - 1) {
+ const bullet = /(?:[*+-]|\d+\.)/;
+ const b = bullet.exec(cap[i + 1])[0];
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+ src = cap.slice(i + 1).join('\n') + src;
+ i = l - 1;
+ }
+ }
+
+ // Determine whether item is loose or not.
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+ // for discount behavior.
+ let loose = next || (/\n\n(?!\s*$)/).test(item);
+ if (i !== l - 1) {
+ next = item.charAt(item.length - 1) === '\n';
+ if (!loose) {
+ loose = next;
+ }
+ }
+
+ this.tokens.push({
+ type: loose ? 'loose_item_start' : 'list_item_start'
+ });
+
+ // Recurse.
+ this.token(item, false, bq);
+
+ this.tokens.push({
+ type: 'list_item_end'
+ });
+ }
+
+ this.tokens.push({
+ type: 'list_end'
+ });
+
+ continue;
+ }
+ }
+
+ // html
+ cap = this.rules.html.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: this.options.sanitize ? 'paragraph' : 'html',
+ pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+ text: cap[0]
+ });
+ continue;
+ }
+
+ // def
+ cap = this.rules.def.exec(src);
+ if ((!bq && top) && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.links[cap[1].toLowerCase()] = {
+ href: cap[2],
+ title: cap[3]
+ };
+ continue;
+ }
+
+ // table (gfm)
+ cap = this.rules.table.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // top-level paragraph
+ cap = this.rules.paragraph.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'paragraph',
+ text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1]
+ });
+ continue;
+ }
+
+ // text
+ cap = this.rules.text.exec(src);
+ if (cap) {
+ // Top-level should never reach here.
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'text',
+ text: cap[0]
+ });
+ continue;
+ }
+
+ if (src) {
+ throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
+ }
+ }
+
+ return this.tokens;
+ }
+}
+
export function format(text, options) {
const markdownOptions = {
renderer: new MattermostMarkdownRenderer(null, options),
@@ -212,7 +459,7 @@ export function format(text, options) {
tables: true
};
- const tokens = marked.lexer(text, markdownOptions);
+ const tokens = new MattermostLexer(markdownOptions).lex(text);
return new MattermostParser(markdownOptions).parse(tokens);
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 575b6d011..8052c000c 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -151,10 +151,14 @@ export function notifyMe(title, body, channel) {
}
}
+var canDing = true;
+
export function ding() {
- if (!isBrowserFirefox()) {
- var audio = new Audio('/static/images/ding.mp3');
+ if (!isBrowserFirefox() && canDing) {
+ var audio = new Audio('/static/images/bing.mp3');
audio.play();
+ canDing = false;
+ setTimeout(() => canDing = true, 3000);
}
}
@@ -477,6 +481,7 @@ export function applyTheme(theme) {
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
}
if (theme.sidebarHeaderTextColor) {
@@ -515,6 +520,7 @@ export function applyTheme(theme) {
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
@@ -548,6 +554,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
@@ -1125,3 +1132,12 @@ export function sortByDisplayName(a, b) {
}
return 0;
}
+
+export function getChannelTerm(channelType) {
+ let channelTerm = 'Channel';
+ if (channelType === Constants.PRIVATE_CHANNEL) {
+ channelTerm = 'Group';
+ }
+
+ return channelTerm;
+}