summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/more_channels.jsx21
-rw-r--r--web/react/components/new_channel_modal.jsx10
-rw-r--r--web/react/components/post_body.jsx9
-rw-r--r--web/react/components/post_info.jsx5
-rw-r--r--web/react/components/post_list.jsx1
-rw-r--r--web/react/components/rhs_comment.jsx9
-rw-r--r--web/react/components/rhs_root_post.jsx13
-rw-r--r--web/react/package.json3
-rw-r--r--web/react/utils/markdown.jsx22
-rw-r--r--web/react/utils/text_formatting.jsx82
-rw-r--r--web/sass-files/sass/partials/_forms.scss6
11 files changed, 138 insertions, 43 deletions
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index ba8be12b2..65cd40975 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -6,6 +6,7 @@ var client = require('../utils/client.jsx');
var asyncClient = require('../utils/async_client.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var LoadingScreen = require('./loading_screen.jsx');
+var NewChannelFlow = require('./new_channel_flow.jsx');
function getStateFromStores() {
return {
@@ -25,6 +26,7 @@ export default class MoreChannels extends React.Component {
var initState = getStateFromStores();
initState.channelType = '';
initState.joiningChannel = -1;
+ initState.showNewChannelModal = false;
this.state = initState;
}
componentDidMount() {
@@ -66,6 +68,7 @@ export default class MoreChannels extends React.Component {
}
handleNewChannel() {
$(React.findDOMNode(this.refs.modal)).modal('hide');
+ this.setState({showNewChannelModal: true});
}
render() {
var serverError;
@@ -148,20 +151,22 @@ export default class MoreChannels extends React.Component {
className='close'
data-dismiss='modal'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
+ <span aria-hidden='true'>{'×'}</span>
+ <span className='sr-only'>{'Close'}</span>
</button>
- <h4 className='modal-title'>More Channels</h4>
+ <h4 className='modal-title'>{'More Channels'}</h4>
<button
- data-toggle='modal'
- data-target='#new_channel'
- data-channeltype={this.state.channelType}
type='button'
className='btn btn-primary channel-create-btn'
onClick={this.handleNewChannel}
>
- Create New Channel
+ {'Create New Channel'}
</button>
+ <NewChannelFlow
+ show={this.state.showNewChannelModal}
+ channelType={this.state.channelType}
+ onModalDismissed={() => this.setState({showNewChannelModal: false})}
+ />
</div>
<div className='modal-body'>
{moreChannels}
@@ -173,7 +178,7 @@ export default class MoreChannels extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Close
+ {'Close'}
</button>
</div>
</div>
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index a8d137bac..c43137744 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -107,8 +107,8 @@ export default class NewChannelModal extends React.Component {
{channelSwitchText}
</div>
<div className={displayNameClass}>
- <label className='col-sm-2 form__label control-label'>{'Name'}</label>
- <div className='col-sm-10'>
+ <label className='col-sm-3 form__label control-label'>{'Name'}</label>
+ <div className='col-sm-9'>
<input
onChange={this.handleChange}
type='text'
@@ -121,7 +121,7 @@ export default class NewChannelModal extends React.Component {
tabIndex='1'
/>
{displayNameError}
- <p className='input__help'>
+ <p className='input__help dark'>
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
@@ -134,11 +134,11 @@ export default class NewChannelModal extends React.Component {
</div>
</div>
<div className='form-group less'>
- <div className='col-sm-2'>
+ <div className='col-sm-3'>
<label className='form__label control-label'>{'Description'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
- <div className='col-sm-10'>
+ <div className='col-sm-9'>
<textarea
className='form-control no-resize'
ref='channel_desc'
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 3be615bb9..e0682e997 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -35,9 +35,7 @@ export default class PostBody extends React.Component {
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
- this.getAllChildNodes(React.findDOMNode(this)).forEach((current) => {
- global.window.emojify.run(current);
- });
+ global.window.emojify.run(React.findDOMNode(this.refs.message_span));
}
componentDidMount() {
@@ -154,17 +152,18 @@ export default class PostBody extends React.Component {
return (
<div className='post-body'>
{comment}
- <p
+ <div
key={`${post.id}_message`}
id={`${post.id}_message`}
className={postClass}
>
{loading}
<span
+ ref='message_span'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
- </p>
+ </div>
{fileAttachmentHolder}
{embed}
</div>
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 34ac9e759..d2a0a4035 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -151,7 +151,10 @@ export default class PostInfo extends React.Component {
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <time className='post-profile-time'>
+ <time
+ className='post-profile-time'
+ title={new Date(post.create_at).toString()}
+ >
{utils.displayDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 94cccaac3..703e548fb 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -83,6 +83,7 @@ export default class PostList extends React.Component {
};
}
componentDidMount() {
+ window.onload = () => this.scrollToBottom();
if (this.props.isActive) {
this.activate();
this.loadFirstPosts(this.props.channelId);
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index ed136c01f..fe31ac381 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -56,6 +56,7 @@ export default class RhsComment extends React.Component {
}
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ global.window.emojify.run(React.findDOMNode(this.refs.message_holder));
}
componentDidMount() {
this.parseEmojis();
@@ -193,7 +194,10 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
- <time className='post-right-comment-time'>
+ <time
+ className='post-profile-time'
+ title={new Date(post.create_at).toString()}
+ >
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
@@ -204,7 +208,8 @@ export default class RhsComment extends React.Component {
<div className='post-body'>
<p className={postClass}>
{loading}
- <span
+ <div
+ ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 85755a85c..2ea697c5b 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -20,6 +20,7 @@ export default class RhsRootPost extends React.Component {
}
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ global.window.emojify.run(React.findDOMNode(this.refs.message_holder));
}
componentDidMount() {
this.parseEmojis();
@@ -132,7 +133,14 @@ export default class RhsRootPost extends React.Component {
<div className='post__content'>
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
- <li className='post-header-col'><time className='post-right-root-time'>{utils.displayCommentDateTime(post.create_at)}</time></li>
+ <li className='post-header-col'>
+ <time
+ className='post-profile-time'
+ title={new Date(post.create_at).toString()}
+ >
+ {utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
{ownerOptions}
@@ -140,7 +148,8 @@ export default class RhsRootPost extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p
+ <div
+ ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>
diff --git a/web/react/package.json b/web/react/package.json
index 04e0f6bab..dd7d45f8a 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -9,7 +9,8 @@
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1",
- "babel-runtime": "5.8.24"
+ "babel-runtime": "5.8.24",
+ "marked": "0.3.5"
},
"devDependencies": {
"browserify": "11.0.1",
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
new file mode 100644
index 000000000..96da54217
--- /dev/null
+++ b/web/react/utils/markdown.jsx
@@ -0,0 +1,22 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const marked = require('marked');
+
+export class MattermostMarkdownRenderer extends marked.Renderer {
+ link(href, title, text) {
+ let outHref = href;
+
+ if (outHref.lastIndexOf('http', 0) !== 0) {
+ outHref = `http://${outHref}`;
+ }
+
+ let output = '<a class="theme" href="' + outHref + '"';
+ if (title) {
+ output += ' title="' + title + '"';
+ }
+ output += '>' + text + '</a>';
+
+ return output;
+ }
+}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 54d010dbf..4e390f708 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -3,21 +3,38 @@
const Autolinker = require('autolinker');
const Constants = require('./constants.jsx');
+const Markdown = require('./markdown.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('./utils.jsx');
+const marked = require('marked');
+
+const markdownRenderer = new Markdown.MattermostMarkdownRenderer();
+
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
// as part of the second parameter:
// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
// - singleline - Specifies whether or not to remove newlines. Defaults to false.
+// - markdown - Enables markdown parsing. Defaults to true.
export function formatText(text, options = {}) {
- let output = sanitizeHtml(text);
+ if (!('markdown' in options)) {
+ options.markdown = true;
+ }
+
+ // wait until marked can sanitize the html so that we don't break markdown block quotes
+ let output;
+ if (!options.markdown) {
+ output = sanitizeHtml(text);
+ } else {
+ output = text;
+ }
+
const tokens = new Map();
// replace important words and phrases with tokens
- output = autolinkUrls(output, tokens);
+ output = autolinkUrls(output, tokens, !!options.markdown);
output = autolinkAtMentions(output, tokens);
output = autolinkHashtags(output, tokens);
@@ -29,11 +46,21 @@ export function formatText(text, options = {}) {
output = highlightCurrentMentions(output, tokens);
}
+ // perform markdown parsing while we have an html-free input string
+ if (options.markdown) {
+ output = marked(output, {
+ renderer: markdownRenderer,
+ sanitize: true
+ });
+ }
+
// reinsert tokens with formatted versions of the important words and phrases
output = replaceTokens(output, tokens);
// replace newlines with html line breaks
- output = replaceNewlines(output, options.singleline);
+ if (options.singleline) {
+ output = replaceNewlines(output);
+ }
return output;
}
@@ -51,7 +78,7 @@ export function sanitizeHtml(text) {
return output;
}
-function autolinkUrls(text, tokens) {
+function autolinkUrls(text, tokens, markdown) {
function replaceUrlWithToken(autolinker, match) {
const linkText = match.getMatchedText();
let url = linkText;
@@ -61,7 +88,7 @@ function autolinkUrls(text, tokens) {
}
const index = tokens.size;
- const alias = `__MM_LINK${index}__`;
+ const alias = `MM_LINK${index}`;
tokens.set(alias, {
value: `<a class='theme' target='_blank' href='${url}'>${linkText}</a>`,
@@ -81,7 +108,30 @@ function autolinkUrls(text, tokens) {
replaceFn: replaceUrlWithToken
});
- return autolinker.link(text);
+ let output = text;
+
+ // temporarily replace markdown links if markdown is enabled so that we don't accidentally parse them twice
+ const markdownLinkTokens = new Map();
+ if (markdown) {
+ function replaceMarkdownLinkWithToken(markdownLink) {
+ const index = markdownLinkTokens.size;
+ const alias = `MM_MARKDOWNLINK${index}`;
+
+ markdownLinkTokens.set(alias, {value: markdownLink});
+
+ return alias;
+ }
+
+ output = output.replace(/\]\([^\)]*\)/g, replaceMarkdownLinkWithToken);
+ }
+
+ output = autolinker.link(output);
+
+ if (markdown) {
+ output = replaceTokens(output, markdownLinkTokens);
+ }
+
+ return output;
}
function autolinkAtMentions(text, tokens) {
@@ -91,7 +141,7 @@ function autolinkAtMentions(text, tokens) {
const usernameLower = username.toLowerCase();
if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) {
const index = tokens.size;
- const alias = `__MM_ATMENTION${index}__`;
+ const alias = `MM_ATMENTION${index}`;
tokens.set(alias, {
value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`,
@@ -119,7 +169,7 @@ function highlightCurrentMentions(text, tokens) {
for (const [alias, token] of tokens) {
if (mentionKeys.indexOf(token.originalText) !== -1) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_SELFMENTION${index}__`;
+ const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
value: `<span class='mention-highlight'>${alias}</span>`,
@@ -138,7 +188,7 @@ function highlightCurrentMentions(text, tokens) {
// look for self mentions in the text
function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
const index = tokens.size;
- const alias = `__MM_SELFMENTION${index}__`;
+ const alias = `MM_SELFMENTION${index}`;
tokens.set(alias, {
value: `<span class='mention-highlight'>${mention}</span>`,
@@ -162,7 +212,7 @@ function autolinkHashtags(text, tokens) {
for (const [alias, token] of tokens) {
if (token.originalText.lastIndexOf('#', 0) === 0) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_HASHTAG${index}__`;
+ const newAlias = `MM_HASHTAG${index}`;
newTokens.set(newAlias, {
value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
@@ -181,7 +231,7 @@ function autolinkHashtags(text, tokens) {
// look for hashtags in the text
function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
const index = tokens.size;
- const alias = `__MM_HASHTAG${index}__`;
+ const alias = `MM_HASHTAG${index}`;
tokens.set(alias, {
value: `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`,
@@ -201,7 +251,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
for (const [alias, token] of tokens) {
if (token.originalText === searchTerm) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_SEARCHTERM${index}__`;
+ const newAlias = `MM_SEARCHTERM${index}`;
newTokens.set(newAlias, {
value: `<span class='search-highlight'>${alias}</span>`,
@@ -219,7 +269,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
function replaceSearchTermWithToken(fullMatch, prefix, word) {
const index = tokens.size;
- const alias = `__MM_SEARCHTERM${index}__`;
+ const alias = `MM_SEARCHTERM${index}`;
tokens.set(alias, {
value: `<span class='search-highlight'>${word}</span>`,
@@ -246,11 +296,7 @@ function replaceTokens(text, tokens) {
return output;
}
-function replaceNewlines(text, singleline) {
- if (!singleline) {
- return text.replace(/\n/g, '<br />');
- }
-
+function replaceNewlines(text) {
return text.replace(/\n/g, ' ');
}
diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss
index 268576a98..c8b08f44d 100644
--- a/web/sass-files/sass/partials/_forms.scss
+++ b/web/sass-files/sass/partials/_forms.scss
@@ -5,9 +5,10 @@
.form__label {
text-align: left;
padding-right: 3px;
- font-weight: bold;
+ font-weight: 600;
font-size: 1.1em;
&.light {
+ font-weight: normal;
color: #999;
font-size: 1.05em;
font-style: italic;
@@ -17,6 +18,9 @@
.input__help {
color: #777;
margin: 10px 0 0 10px;
+ &.dark {
+ color: #222;
+ }
&.error {
color: #a94442;
}