summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx9
-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/user_settings/user_settings_general.jsx90
-rw-r--r--web/react/stores/user_store.jsx9
-rw-r--r--web/react/utils/highlight.jsx51
-rw-r--r--web/react/utils/markdown.jsx351
-rw-r--r--web/react/utils/utils.jsx14
11 files changed, 930 insertions, 138 deletions
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/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/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/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/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 fff9c460b..8052c000c 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -155,7 +155,7 @@ var canDing = true;
export function ding() {
if (!isBrowserFirefox() && canDing) {
- var audio = new Audio('/static/images/ding.mp3');
+ var audio = new Audio('/static/images/bing.mp3');
audio.play();
canDing = false;
setTimeout(() => canDing = true, 3000);
@@ -481,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) {
@@ -519,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) {
@@ -552,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);
@@ -1129,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;
+}