diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/more_channels.jsx | 21 | ||||
-rw-r--r-- | web/react/components/new_channel_modal.jsx | 10 | ||||
-rw-r--r-- | web/react/components/post_body.jsx | 9 | ||||
-rw-r--r-- | web/react/components/post_info.jsx | 5 | ||||
-rw-r--r-- | web/react/components/post_list.jsx | 1 | ||||
-rw-r--r-- | web/react/components/rhs_comment.jsx | 9 | ||||
-rw-r--r-- | web/react/components/rhs_root_post.jsx | 13 | ||||
-rw-r--r-- | web/react/package.json | 3 | ||||
-rw-r--r-- | web/react/utils/markdown.jsx | 22 | ||||
-rw-r--r-- | web/react/utils/text_formatting.jsx | 82 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_forms.scss | 6 |
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'>×</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; } |