summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--api/channel_test.go4
-rw-r--r--doc/developer/code-contribution.md11
-rw-r--r--store/sql_channel_store.go4
-rw-r--r--store/sql_store.go4
-rw-r--r--web/react/components/new_channel_modal.jsx10
-rw-r--r--web/react/components/post_body.jsx4
-rw-r--r--web/react/package.json3
-rw-r--r--web/react/utils/markdown.jsx16
-rw-r--r--web/react/utils/text_formatting.jsx82
-rw-r--r--web/sass-files/sass/partials/_forms.scss6
11 files changed, 114 insertions, 34 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1be9388b..18238d9eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,10 @@ Performance
Code Quality
- Reformatted Javascript per Mattermost Style Guide
+
+UI
+
+- Added version, build number, build date and build hash under Account Settings -> Security (to be moved to "About" dialog later)
### Bug Fixes
diff --git a/api/channel_test.go b/api/channel_test.go
index 7e9267192..14bfe1cf7 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -57,7 +57,7 @@ func TestCreateChannel(t *testing.T) {
rchannel.Data.(*model.Channel).Id = ""
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err != nil {
- if err.Message != "A channel with that handle already exists" {
+ if err.Message != "A channel with that URL already exists" {
t.Fatal(err)
}
}
@@ -68,7 +68,7 @@ func TestCreateChannel(t *testing.T) {
Client.DeleteChannel(savedId)
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err != nil {
- if err.Message != "A channel with that handle was previously created" {
+ if err.Message != "A channel with that URL was previously created" {
t.Fatal(err)
}
}
diff --git a/doc/developer/code-contribution.md b/doc/developer/code-contribution.md
index e9b088beb..325a67546 100644
--- a/doc/developer/code-contribution.md
+++ b/doc/developer/code-contribution.md
@@ -35,7 +35,16 @@ git checkout -b <branch name>
For pull requests made by contributors not yet added to the approved contributor list, a reviewer may respond:
- ```Thanks @[username] for the pull request! Before we can review, we need to add you to the list of [approved contributors](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true). Please help complete the Mattermost [contribution license agreement](http://www.mattermost.org/mattermost-contributor-agreement/), which is a standard procedure for many open source projects. More information is in the above link. Please let us know if you have any questions. We are very happy to have you join our growing community!```
+ ```
+ Thanks @[username] for the pull request!
+
+ Before we can review, we need to add you to the list of [approved contributors](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true).
+ Please help complete the Mattermost [contribution license agreement](http://www.mattermost.org/mattermost-contributor-agreement/), which is a standard procedure for many open source projects. More information is in the above link.
+
+ Please let us know if you have any questions.
+
+ We are very happy to have you join our growing community!
+```
2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index bad878501..877246fc3 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -85,9 +85,9 @@ func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel {
dupChannel := model.Channel{}
s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name = :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name})
if dupChannel.DeleteAt > 0 {
- result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that handle was previously created", "id="+channel.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL was previously created", "id="+channel.Id+", "+err.Error())
} else {
- result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that handle already exists", "id="+channel.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL already exists", "id="+channel.Id+", "+err.Error())
}
} else {
result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error())
diff --git a/store/sql_store.go b/store/sql_store.go
index 2f7ac2805..adac47b4d 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -129,12 +129,12 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
db, err := dbsql.Open(driver, dataSource)
if err != nil {
- l4g.Critical("Failed to open sql connection to '%v' err:%v", dataSource, err)
+ l4g.Critical("Failed to open sql connection to err:%v", err)
time.Sleep(time.Second)
panic("Failed to open sql connection" + err.Error())
}
- l4g.Info("Pinging sql %v database at '%v'", con_type, dataSource)
+ l4g.Info("Pinging sql %v database", con_type)
err = db.Ping()
if err != nil {
l4g.Critical("Failed to ping db err:%v", err)
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index fc7b8c183..1488a1431 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..8020714cd 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -154,7 +154,7 @@ 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}
@@ -164,7 +164,7 @@ export default class PostBody extends React.Component {
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
- </p>
+ </div>
{fileAttachmentHolder}
{embed}
</div>
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..8c63810cd
--- /dev/null
+++ b/web/react/utils/markdown.jsx
@@ -0,0 +1,16 @@
+// 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}`;
+ }
+
+ return super.link(outHref, title, text);
+ }
+}
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;
}