summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--doc/developer/style-guide.md19
-rw-r--r--web/react/components/channel_header.jsx8
-rw-r--r--web/react/components/message_wrapper.jsx10
-rw-r--r--web/react/components/navbar.jsx2
-rw-r--r--web/react/components/new_channel_modal.jsx2
-rw-r--r--web/react/components/post_body.jsx8
-rw-r--r--web/react/components/rhs_comment.jsx10
-rw-r--r--web/react/components/rhs_root_post.jsx7
-rw-r--r--web/react/components/search_results_item.jsx16
-rw-r--r--web/react/utils/text_formatting.jsx268
-rw-r--r--web/react/utils/utils.jsx201
12 files changed, 323 insertions, 230 deletions
diff --git a/Makefile b/Makefile
index ea9fc21d4..137bb0a82 100644
--- a/Makefile
+++ b/Makefile
@@ -157,6 +157,8 @@ clean:
rm -rf api/data/*
rm -rf logs/*
+ rm -rf Godeps/_workspace/pkg/
+
run: install
mkdir -p web/static/js
diff --git a/doc/developer/style-guide.md b/doc/developer/style-guide.md
index 470788cf5..a06f9e29b 100644
--- a/doc/developer/style-guide.md
+++ b/doc/developer/style-guide.md
@@ -14,11 +14,13 @@ In addition all code must be run though the official go formatter tool [gofmt](h
## Javascript
-Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors or warnings running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](https://github.com/mattermost/platform/blob/master/web/react/.eslintrc.json)
+Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](/web/react/.eslintrc)
Instructions on how to use ESLint with your favourite editor can be found here: [http://eslint.org/docs/user-guide/integrations](http://eslint.org/docs/user-guide/integrations)
-The following is an abridged version of the [Airbnb Javascript Style Guide](https://github.com/airbnb/javascript/blob/master/README.md#airbnb-javascript-style-guide-), with modifications. Anything that is unclear here follow that guide. If there is a conflict, follow what is said below.
+You can run eslint using the makefile by using `make check`
+
+The following is a subset of what ESLint checks for. ESLint is always the authority.
### Whitespace
@@ -53,10 +55,10 @@ function myFunction ( parm1, parm2 ){
```javascript
// Correct
-var x = 1;
+let x = 1;
// Incorrect
-var x = 1
+let x = 1
```
### Variables
@@ -110,7 +112,7 @@ if (something)
### Strings
-- Use template strings instead of concatenation.
+- Use of template strings is preferred instead of concatenation.
```javascript
// Correct
@@ -126,17 +128,18 @@ function wrongGetStr(stuff) {
## React-JSX
-Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors or warnings running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](https://github.com/mattermost/platform/blob/master/web/react/.eslintrc.json)
+Part of the build process is running ESLint. ESLint is the final authority on all style issues. PRs will not be accepted unless there are no errors running ESLint. The ESLint configuration file can be found in: [web/react/.eslintrc](/web/react/.eslintrc)
Instructions on how to use ESLint with your favourite editor can be found here: [http://eslint.org/docs/user-guide/integrations](http://eslint.org/docs/user-guide/integrations)
-This is an abridged version of the [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react#airbnb-reactjsx-style-guide). Anything that is unclear here follow that guide. If there is a conflict, follow what is said below.
+You can run eslint using the makefile by using `make check`
+
+The following is a subset of what ESLint checks for. ESLint is always the authority.
### General
- Include only one React component per file.
- Use class \<name\> extends React.Component over React.createClass unless you need mixins
-- CapitalCamelCase with .jsx extension for component filenames.
- Filenames should be the component name.
### Alignment
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index db23a5831..0dbbc20d4 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
+const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
@@ -107,7 +108,6 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
- const description = Utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true});
const popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>);
let channelTitle = channel.display_name;
const currentId = UserStore.getCurrentId();
@@ -326,9 +326,9 @@ export default class ChannelHeader extends React.Component {
data-toggle='popover'
data-content={popoverContent}
className='description'
- >
- {description}
- </div>
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
+ />
</div>
</th>
<th>
diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx
index bce305853..5adf4f228 100644
--- a/web/react/components/message_wrapper.jsx
+++ b/web/react/components/message_wrapper.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
export default class MessageWrapper extends React.Component {
constructor(props) {
@@ -10,10 +10,7 @@ export default class MessageWrapper extends React.Component {
}
render() {
if (this.props.message) {
- var inner = Utils.textToJsx(this.props.message, this.props.options);
- return (
- <div>{inner}</div>
- );
+ return <div dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, this.props.options)}}/>;
}
return <div/>;
@@ -21,8 +18,7 @@ export default class MessageWrapper extends React.Component {
}
MessageWrapper.defaultProps = {
- message: null,
- options: null
+ message: ''
};
MessageWrapper.propTypes = {
message: React.PropTypes.string,
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 2258bf2b3..cae9f12e4 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -332,7 +332,7 @@ export default class Navbar extends React.Component {
popoverContent = React.renderToString(
<MessageWrapper
message={channel.description}
- options={{singleline: true, noMentionHighlight: true}}
+ options={{singleline: true, mentionHighlight: false}}
/>
);
isAdmin = this.state.member.roles.indexOf('admin') > -1;
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index 2bf645dc5..f3fb8da2a 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -127,7 +127,7 @@ export default class NewChannelModal extends React.Component {
href='#'
onClick={this.props.onChangeURLPressed}
>
- {'change this URL'}
+ {'Edit'}
</a>
{')'}
</p>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index d9b8f20ce..df4ed3d57 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -5,6 +5,7 @@ const FileAttachmentList = require('./file_attachment_list.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
+const TextFormatting = require('../utils/text_formatting.jsx');
const twemoji = require('twemoji');
export default class PostBody extends React.Component {
@@ -33,7 +34,6 @@ export default class PostBody extends React.Component {
const post = this.props.post;
const filenames = this.props.post.filenames;
const parentPost = this.props.parentPost;
- const inner = Utils.textToJsx(this.state.message);
let comment = '';
let postClass = '';
@@ -135,7 +135,11 @@ export default class PostBody extends React.Component {
key={`${post.id}_message`}
className={postClass}
>
- {loading}<span>{inner}</span>
+ {loading}
+ <span
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ />
</p>
{fileAttachmentHolder}
{embed}
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index f1a90102c..ed136c01f 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -12,6 +12,7 @@ var FileAttachmentList = require('./file_attachment_list.jsx');
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var ActionTypes = Constants.ActionTypes;
+var TextFormatting = require('../utils/text_formatting.jsx');
var twemoji = require('twemoji');
export default class RhsComment extends React.Component {
@@ -84,7 +85,6 @@ export default class RhsComment extends React.Component {
type = 'Comment';
}
- var message = Utils.textToJsx(post.message);
var timestamp = UserStore.getCurrentUser().update_at;
var loading;
@@ -202,7 +202,13 @@ export default class RhsComment extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p className={postClass}>{loading}{message}</p>
+ <p className={postClass}>
+ {loading}
+ <span
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ </p>
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 83b57b955..85755a85c 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -4,6 +4,7 @@
var ChannelStore = require('../stores/channel_store.jsx');
var UserProfile = require('./user_profile.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
var utils = require('../utils/utils.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var twemoji = require('twemoji');
@@ -35,7 +36,6 @@ export default class RhsRootPost extends React.Component {
}
render() {
var post = this.props.post;
- var message = utils.textToJsx(post.message);
var isOwner = UserStore.getCurrentId() === post.user_id;
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -140,7 +140,10 @@ export default class RhsRootPost extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p>{message}</p>
+ <p
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index aa56f1174..0e951f5c6 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -10,6 +10,7 @@ var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
var ActionTypes = Constants.ActionTypes;
export default class SearchResultsItem extends React.Component {
@@ -56,7 +57,6 @@ export default class SearchResultsItem extends React.Component {
}
render() {
- var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch});
var channelName = '';
var channel = ChannelStore.get(this.props.post.channel_id);
var timestamp = UserStore.getCurrentUser().update_at;
@@ -68,6 +68,11 @@ export default class SearchResultsItem extends React.Component {
}
}
+ const formattingOptions = {
+ searchTerm: this.props.term,
+ mentionHighlight: this.props.isMentionSearch
+ };
+
return (
<div
className='search-item-container post'
@@ -91,7 +96,12 @@ export default class SearchResultsItem extends React.Component {
</time>
</li>
</ul>
- <div className='search-item-snippet'><span>{message}</span></div>
+ <div className='search-item-snippet'>
+ <span
+ onClick={this.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ />
+ </div>
</div>
</div>
);
@@ -102,4 +112,4 @@ SearchResultsItem.propTypes = {
post: React.PropTypes.object,
isMentionSearch: React.PropTypes.bool,
term: React.PropTypes.string
-}; \ No newline at end of file
+};
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
new file mode 100644
index 000000000..2c67d7a46
--- /dev/null
+++ b/web/react/utils/text_formatting.jsx
@@ -0,0 +1,268 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Autolinker = require('autolinker');
+const Constants = require('./constants.jsx');
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('./utils.jsx');
+
+// 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.
+export function formatText(text, options = {}) {
+ let output = sanitizeHtml(text);
+ const tokens = new Map();
+
+ // replace important words and phrases with tokens
+ output = autolinkUrls(output, tokens);
+ output = autolinkAtMentions(output, tokens);
+ output = autolinkHashtags(output, tokens);
+
+ if (options.searchTerm) {
+ output = highlightSearchTerm(output, tokens, options.searchTerm);
+ }
+
+ if (!('mentionHighlight' in options) || options.mentionHighlight) {
+ output = highlightCurrentMentions(output, tokens);
+ }
+
+ // 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);
+
+ return output;
+}
+
+export function sanitizeHtml(text) {
+ let output = text;
+
+ // normal string.replace only does a single occurrance so use a regex instead
+ output = output.replace(/&/g, '&amp;');
+ output = output.replace(/</g, '&lt;');
+ output = output.replace(/>/g, '&gt;');
+ output = output.replace(/'/g, '&apos;');
+ output = output.replace(/"/g, '&quot;');
+
+ return output;
+}
+
+function autolinkUrls(text, tokens) {
+ function replaceUrlWithToken(autolinker, match) {
+ const linkText = match.getMatchedText();
+ let url = linkText;
+
+ if (!url.startsWith('http')) {
+ url = `http://${linkText}`;
+ }
+
+ const index = tokens.size;
+ const alias = `__MM_LINK${index}__`;
+
+ tokens.set(alias, {
+ value: `<a class='theme' target='_blank' href='${url}'>${linkText}</a>`,
+ originalText: linkText
+ });
+
+ return alias;
+ }
+
+ // we can't just use a static autolinker because we need to set replaceFn
+ const autolinker = new Autolinker({
+ urls: true,
+ email: true,
+ phone: false,
+ twitter: false,
+ hashtag: false,
+ replaceFn: replaceUrlWithToken
+ });
+
+ return autolinker.link(text);
+}
+
+function autolinkAtMentions(text, tokens) {
+ let output = text;
+
+ function replaceAtMentionWithToken(fullMatch, prefix, mention, username) {
+ const usernameLower = username.toLowerCase();
+ if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) {
+ const index = tokens.size;
+ const alias = `__MM_ATMENTION${index}__`;
+
+ tokens.set(alias, {
+ value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`,
+ originalText: mention
+ });
+
+ return prefix + alias;
+ }
+
+ return fullMatch;
+ }
+
+ output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken);
+
+ return output;
+}
+
+function highlightCurrentMentions(text, tokens) {
+ let output = text;
+
+ const mentionKeys = UserStore.getCurrentMentionKeys();
+
+ // look for any existing tokens which are self mentions and should be highlighted
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (mentionKeys.indexOf(token.originalText) !== -1) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `__MM_SELFMENTION${index}__`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='mention-highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for self mentions in the text
+ function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
+ const index = tokens.size;
+ const alias = `__MM_SELFMENTION${index}__`;
+
+ tokens.set(alias, {
+ value: `<span class='mention-highlight'>${mention}</span>`,
+ originalText: mention
+ });
+
+ return prefix + alias;
+ }
+
+ for (const mention of UserStore.getCurrentMentionKeys()) {
+ output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), replaceCurrentMentionWithToken);
+ }
+
+ return output;
+}
+
+function autolinkHashtags(text, tokens) {
+ let output = text;
+
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText.startsWith('#')) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `__MM_HASHTAG${index}__`;
+
+ newTokens.set(newAlias, {
+ value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for hashtags in the text
+ function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
+ const index = tokens.size;
+ const alias = `__MM_HASHTAG${index}__`;
+
+ tokens.set(alias, {
+ value: `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`,
+ originalText: hashtag
+ });
+
+ return prefix + alias;
+ }
+
+ return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, replaceHashtagWithToken);
+}
+
+function highlightSearchTerm(text, tokens, searchTerm) {
+ let output = text;
+
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText === searchTerm) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `__MM_SEARCHTERM${index}__`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='search-highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ function replaceSearchTermWithToken(fullMatch, prefix, word) {
+ const index = tokens.size;
+ const alias = `__MM_SEARCHTERM${index}__`;
+
+ tokens.set(alias, {
+ value: `<span class='search-highlight'>${word}</span>`,
+ originalText: word
+ });
+
+ return prefix + alias;
+ }
+
+ return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken);
+}
+
+function replaceTokens(text, tokens) {
+ let output = text;
+
+ // iterate backwards through the map so that we do replacement in the opposite order that we added tokens
+ const aliases = [...tokens.keys()];
+ for (let i = aliases.length - 1; i >= 0; i--) {
+ const alias = aliases[i];
+ const token = tokens.get(alias);
+ output = output.replace(alias, token.value);
+ }
+
+ return output;
+}
+
+function replaceNewlines(text, singleline) {
+ if (!singleline) {
+ return text.replace(/\n/g, '<br />');
+ }
+
+ return text.replace(/\n/g, ' ');
+}
+
+// A click handler that can be used with the results of TextFormatting.formatText to add default functionality
+// to clicked hashtags and @mentions.
+export function handleClick(e) {
+ const mentionAttribute = e.target.getAttributeNode('data-mention');
+ const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
+
+ if (mentionAttribute) {
+ Utils.searchForTerm(mentionAttribute.value);
+ } else if (hashtagAttribute) {
+ Utils.searchForTerm(hashtagAttribute.value);
+ }
+}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 2076d7842..c2307f5e9 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -434,205 +434,6 @@ export function searchForTerm(term) {
});
}
-var puncStartRegex = /^((?![@#])\W)+/g;
-var puncEndRegex = /(\W)+$/g;
-
-export function textToJsx(textin, options) {
- var text = textin;
- if (options && options.singleline) {
- var repRegex = new RegExp('\n', 'g'); //eslint-disable-line no-control-regex
- text = text.replace(repRegex, ' ');
- }
-
- var searchTerm = '';
- if (options && options.searchTerm) {
- searchTerm = options.searchTerm.toLowerCase();
- }
-
- var mentionClass = 'mention-highlight';
- if (options && options.noMentionHighlight) {
- mentionClass = '';
- }
-
- var inner = [];
-
- // Function specific regex
- var hashRegex = /^href="#[^']+"|(^#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
-
- var implicitKeywords = UserStore.getCurrentMentionKeys();
-
- var lines = text.split('\n');
- for (let i = 0; i < lines.length; i++) {
- var line = lines[i];
- var words = line.split(' ');
- var highlightSearchClass = '';
- for (let z = 0; z < words.length; z++) {
- var word = words[z];
- var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
- var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
- var explicitMention = mentionRegex.exec(trimWord);
-
- if (searchTerm !== '') {
- let searchWords = searchTerm.split(' ');
- for (let idx in searchWords) {
- if ({}.hasOwnProperty.call(searchWords, idx)) {
- let searchWord = searchWords[idx];
- if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- break;
- } else if (searchWord.charAt(searchWord.length - 1) === '*') {
- let searchWordPrefix = searchWord.slice(0, -1);
- if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) {
- highlightSearchClass = ' search-highlight';
- break;
- }
- }
- }
- }
- }
-
- if (explicitMention &&
- (UserStore.getProfileByUsername(explicitMention[1]) ||
- Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1)) {
- let name = explicitMention[1];
-
- // do both a non-case sensitive and case senstive check
- let mClass = '';
- if (implicitKeywords.indexOf('@' + name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@' + name) !== -1) {
- mClass = mentionClass;
- }
-
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
-
- if (searchTerm === name) {
- highlightSearchClass = ' search-highlight';
- }
-
- inner.push(
- <span key={name + i + z + '_span'}>
- {prefix}
- <a
- className={mClass + highlightSearchClass + ' mention-link'}
- key={name + i + z + '_link'}
- href='#'
- onClick={() => searchForTerm(name)} //eslint-disable-line no-loop-func
- >
- @{name}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (testUrlMatch(word).length) {
- let match = testUrlMatch(word)[0];
- let link = match.link;
-
- let prefix = word.substring(0, word.indexOf(match.text));
- let suffix = word.substring(word.indexOf(match.text) + match.text.length);
-
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- key={word + i + z + '_link'}
- className={'theme' + highlightSearchClass}
- target='_blank'
- href={link}
- >
- {match.text}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (trimWord.match(hashRegex)) {
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
- let mClass = '';
- if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
- mClass = mentionClass;
- }
-
- if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- }
-
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- key={word + i + z + '_hash'}
- className={'theme ' + mClass + highlightSearchClass}
- href='#'
- onClick={searchForTerm.bind(this, trimWord)} //eslint-disable-line no-loop-func
- >
- {trimWord}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
-
- if (trimWord.charAt(0) === '@') {
- if (searchTerm === trimWord.substring(1).toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- }
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- className={mentionClass + highlightSearchClass}
- key={name + i + z + '_link'}
- href='#'
- >
- {trimWord}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else {
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <span className={mentionClass + highlightSearchClass}>
- {replaceHtmlEntities(trimWord)}
- </span>
- {suffix}
- {' '}
- </span>
- );
- }
- } else if (word === '') {
-
- // if word is empty dont include a span
-
- } else {
- inner.push(
- <span key={word + i + z + '_span'}>
- <span className={highlightSearchClass}>
- {replaceHtmlEntities(word)}
- </span>
- {' '}
- </span>
- );
- }
- highlightSearchClass = '';
- }
- if (i !== lines.length - 1) {
- inner.push(
- <br key={'br_' + i}/>
- );
- }
- }
-
- return inner;
-}
-
export function getFileType(extin) {
var ext = extin.toLowerCase();
if (Constants.IMAGE_TYPES.indexOf(ext) > -1) {
@@ -1133,6 +934,6 @@ export function getTeamURLFromAddressBar() {
export function getShortenedTeamURL() {
const teamURL = getTeamURLFromAddressBar();
if (teamURL.length > 24) {
- return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length - 1) + '/';
+ return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/';
}
}