Merge branch 'master' into PLT-25

Этот коммит содержится в:
=Corey Hulen
2015-10-26 22:11:42 -07:00
родитель e750a8fd36 28847c6b4b
Коммит aec99ceb9d
49 изменённых файлов: 1251 добавлений и 281 удалений

Просмотреть файл

@@ -147,7 +147,7 @@ export default class CreateComment extends React.Component {
}
const t = Date.now();
if ((t - this.lastTime) > 5000) {
if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
this.lastTime = t;
}

Просмотреть файл

@@ -38,6 +38,7 @@ export default class CreatePost extends React.Component {
this.getFileCount = this.getFileCount.bind(this);
this.handleArrowUp = this.handleArrowUp.bind(this);
this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
@@ -122,6 +123,11 @@ export default class CreatePost extends React.Component {
post.message,
false,
(data) => {
if (data.response === 'not implemented') {
this.sendMessage(post);
return;
}
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
@@ -130,63 +136,70 @@ export default class CreatePost extends React.Component {
}
},
(err) => {
const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
if (err.sendMessage) {
this.sendMessage(post);
} else {
const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
}
}
);
} else {
post.channel_id = this.state.channelId;
post.filenames = this.state.previews;
const time = Utils.getTimestamp();
const userId = UserStore.getCurrentId();
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
post.root_id = this.state.rootId;
post.parent_id = this.state.parentId;
const channel = ChannelStore.get(this.state.channelId);
PostStore.storePendingPost(post);
PostStore.storeDraft(channel.id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
(data) => {
AsyncClient.getPosts();
const member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST,
post: data
});
},
(err) => {
const state = {};
if (err.message === 'Invalid RootId parameter') {
if ($('#post_deleted').length > 0) {
$('#post_deleted').modal('show');
}
PostStore.removePendingPost(post.pending_post_id);
} else {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
}
state.submitting = false;
this.setState(state);
}
);
this.sendMessage(post);
}
}
sendMessage(post) {
post.channel_id = this.state.channelId;
post.filenames = this.state.previews;
const time = Utils.getTimestamp();
const userId = UserStore.getCurrentId();
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
post.root_id = this.state.rootId;
post.parent_id = this.state.parentId;
const channel = ChannelStore.get(this.state.channelId);
PostStore.storePendingPost(post);
PostStore.storeDraft(channel.id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
(data) => {
AsyncClient.getPosts();
const member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST,
post: data
});
},
(err) => {
const state = {};
if (err.message === 'Invalid RootId parameter') {
if ($('#post_deleted').length > 0) {
$('#post_deleted').modal('show');
}
PostStore.removePendingPost(post.pending_post_id);
} else {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
}
state.submitting = false;
this.setState(state);
}
);
}
postMsgKeyPress(e) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
@@ -195,7 +208,7 @@ export default class CreatePost extends React.Component {
}
const t = Date.now();
if ((t - this.lastTime) > 5000) {
if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
this.lastTime = t;
}
@@ -240,8 +253,14 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
let message = err;
if (message && typeof message !== 'string') {
// err is an AppError from the server
message = err.message;
}
if (clientId === -1) {
this.setState({serverError: err});
this.setState({serverError: message});
} else {
const draft = PostStore.getDraft(this.state.channelId);
@@ -252,7 +271,7 @@ export default class CreatePost extends React.Component {
PostStore.storeDraft(this.state.channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
}
}
handleTextDrop(text) {

Просмотреть файл

@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
this.prevTimer = null;
this.state = ErrorStore.getLastError();
if (this.isValidError(this.state)) {
this.prevTimer = setTimeout(this.handleClose, 10000);
}
}
isValidError(s) {
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
onErrorChange() {
var newState = ErrorStore.getLastError();
if (this.prevTimer != null) {
clearInterval(this.prevTimer);
this.prevTimer = null;
}
if (newState) {
this.setState(newState);
if (!this.isConnectionError(newState)) {
this.prevTimer = setTimeout(this.handleClose, 10000);
}
} else {
this.setState({message: null});
}

Просмотреть файл

@@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component {
constructor(props) {
super(props);
this.timer = null;
this.lastTime = 0;
this.onChange = this.onChange.bind(this);
this.updateTypingText = this.updateTypingText.bind(this);
this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
this.typingUsers = {};
this.state = {
text: ''
};
@@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component {
componentWillReceiveProps(newProps) {
if (this.props.channelId !== newProps.channelId) {
this.setState({text: ''});
this.updateTypingText();
}
}
@@ -36,30 +36,53 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
let username = 'Someone';
if (msg.action === SocketEvents.TYPING &&
this.props.channelId === msg.channel_id &&
this.props.parentId === msg.props.parent_id) {
this.lastTime = new Date().getTime();
var username = 'Someone';
if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
this.setState({text: username + ' is typing...'});
if (!this.timer) {
this.timer = setInterval(function myTimer() {
if ((new Date().getTime() - this.lastTime) > 8000) {
this.setState({text: ''});
}
}.bind(this), 3000);
if (this.typingUsers[username]) {
clearTimeout(this.typingUsers[username]);
}
this.typingUsers[username] = setTimeout(function myTimer(user) {
delete this.typingUsers[user];
this.updateTypingText();
}.bind(this, username), Constants.UPDATE_TYPING_MS);
this.updateTypingText();
} else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
this.setState({text: ''});
if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
clearTimeout(this.typingUsers[username]);
delete this.typingUsers[username];
this.updateTypingText();
}
}
updateTypingText() {
const users = Object.keys(this.typingUsers);
let text = '';
switch (users.length) {
case 0:
text = '';
break;
case 1:
text = users[0] + ' is typing...';
break;
default:
const last = users.pop();
text = users.join(', ') + ' and ' + last + ' are typing...';
break;
}
this.setState({text});
}
render() {
return (
<span className='msg-typing'>{this.state.text}</span>

Просмотреть файл

@@ -105,8 +105,15 @@ export default class SearchBar extends React.Component {
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
// append * if not present
let searchTerms = terms;
if (searchTerms.search(/\*\s*$/) === -1) {
searchTerms = searchTerms + '*';
}
client.search(
terms,
searchTerms,
(data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {

Просмотреть файл

@@ -40,6 +40,9 @@ export default class Sidebar extends React.Component {
this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
this.updateTitle = this.updateTitle.bind(this);
this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -48,8 +51,45 @@ export default class Sidebar extends React.Component {
state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
this.state = state;
this.unreadCountPerChannel = {};
this.setUnreadCountPerChannel();
}
setUnreadCountPerChannel() {
const channels = ChannelStore.getAll();
const members = ChannelStore.getAllMembers();
const channelUnreadCounts = {};
channels.forEach((ch) => {
const chMember = members[ch.id];
let chMentionCount = chMember.mention_count;
let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
if (ch.type === 'D') {
chMentionCount = chUnreadCount;
chUnreadCount = 0;
}
channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
});
this.unreadCountPerChannel = channelUnreadCounts;
}
getUnreadCount(channelId) {
let mentions = 0;
let msgs = 0;
if (channelId) {
return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
}
Object.keys(this.unreadCountPerChannel).forEach((chId) => {
msgs += this.unreadCountPerChannel[chId].msgs;
mentions += this.unreadCountPerChannel[chId].mentions;
});
return {msgs, mentions};
}
getStateFromStores() {
const members = ChannelStore.getAllMembers();
@@ -192,7 +232,10 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
const unread = this.getUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
}
}
onScroll() {
@@ -273,6 +316,7 @@ export default class Sidebar extends React.Component {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
var unreadCount = this.getUnreadCount(channel.id);
var msgCount;
var linkClass = '';
@@ -284,7 +328,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
msgCount = unreadCount.msgs + unreadCount.mentions;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
@@ -301,16 +345,8 @@ export default class Sidebar extends React.Component {
var badge = null;
if (channelMember) {
if (channel.type === 'D') {
// direct message channels show badges for any number of unread posts
msgCount = channel.total_msg_count - channelMember.msg_count;
if (msgCount > 0) {
badge = <span className='badge pull-right small'>{msgCount}</span>;
this.badgesActive = true;
}
} else if (channelMember.mention_count > 0) {
// public and private channels only show badges for mentions
badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
if (unreadCount.mentions) {
badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
@@ -434,6 +470,8 @@ export default class Sidebar extends React.Component {
render() {
this.badgesActive = false;
this.setUnreadCountPerChannel();
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;

Просмотреть файл

@@ -0,0 +1,55 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
var Constants = require('../../utils/constants.jsx');
export default class CodeThemeChooser extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const theme = this.props.theme;
const premadeThemes = [];
for (const k in Constants.CODE_THEMES) {
if (Constants.CODE_THEMES.hasOwnProperty(k)) {
let activeClass = '';
if (k === theme.codeTheme) {
activeClass = 'active';
}
premadeThemes.push(
<div
className='col-xs-6 col-sm-3 premade-themes'
key={'premade-theme-key' + k}
>
<div
className={activeClass}
onClick={() => this.props.updateTheme(k)}
>
<label>
<img
className='img-responsive'
src={'/static/images/themes/code_themes/' + k + '.png'}
/>
<div className='theme-label'>{Constants.CODE_THEMES[k]}</div>
</label>
</div>
</div>
);
}
}
return (
<div className='row'>
{premadeThemes}
</div>
);
}
}
CodeThemeChooser.propTypes = {
theme: React.PropTypes.object.isRequired,
updateTheme: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -7,6 +7,7 @@ var Utils = require('../../utils/utils.jsx');
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
const CodeThemeChooser = require('./code_theme_chooser.jsx');
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
@@ -18,12 +19,14 @@ export default class UserSettingsAppearance extends React.Component {
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
this.updateCodeTheme = this.updateCodeTheme.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
this.state = this.getStateFromStores();
this.originalTheme = this.state.theme;
this.originalCodeTheme = this.state.theme.codeTheme;
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -58,6 +61,10 @@ export default class UserSettingsAppearance extends React.Component {
type = 'custom';
}
if (!theme.codeTheme) {
theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
return {theme, type};
}
onChange() {
@@ -93,6 +100,13 @@ export default class UserSettingsAppearance extends React.Component {
);
}
updateTheme(theme) {
theme.codeTheme = this.state.theme.codeTheme;
this.setState({theme});
Utils.applyTheme(theme);
}
updateCodeTheme(codeTheme) {
var theme = this.state.theme;
theme.codeTheme = codeTheme;
this.setState({theme});
Utils.applyTheme(theme);
}
@@ -102,6 +116,7 @@ export default class UserSettingsAppearance extends React.Component {
handleClose() {
const state = this.getStateFromStores();
state.serverError = null;
state.theme.codeTheme = this.originalCodeTheme;
Utils.applyTheme(state.theme);
@@ -170,7 +185,13 @@ export default class UserSettingsAppearance extends React.Component {
</div>
{custom}
<hr />
{serverError}
<strong className='radio'>{'Code Theme'}</strong>
<CodeThemeChooser
theme={this.state.theme}
updateTheme={this.updateCodeTheme}
/>
<hr />
{serverError}
<a
className='btn btn-sm btn-primary'
href='#'

Просмотреть файл

@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}.bind(this),
function imageUploadFailure(err) {
var state = this.setupInitialState(this.props);
state.serverError = err;
state.serverError = err.message;
this.setState(state);
}.bind(this)
);

Просмотреть файл

@@ -6,6 +6,7 @@
"autolinker": "0.18.1",
"babel-runtime": "5.8.24",
"flux": "2.1.1",
"highlight.js": "^8.9.1",
"keymirror": "0.1.1",
"marked": "0.3.5",
"object-assign": "3.0.0",

Просмотреть файл

@@ -86,7 +86,7 @@ class SocketStoreClass extends EventEmitter {
this.failCount = this.failCount + 1;
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.'});
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'});
ErrorStore.emitChange();
};
@@ -160,7 +160,7 @@ function handleNewPostEvent(msg) {
if (window.isActive) {
AsyncClient.updateLastViewedAt(true);
}
} else {
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id);
}

Просмотреть файл

@@ -132,7 +132,7 @@ export function getChannel(id) {
callTracker['getChannel' + id] = utils.getTimestamp();
client.getChannel(id,
function getChannelSuccess(data, textStatus, xhr) {
(data, textStatus, xhr) => {
callTracker['getChannel' + id] = 0;
if (xhr.status === 304 || !data) {
@@ -145,7 +145,7 @@ export function getChannel(id) {
member: data.member
});
},
function getChannelFailure(err) {
(err) => {
callTracker['getChannel' + id] = 0;
dispatchError(err, 'getChannel');
}

Просмотреть файл

@@ -34,7 +34,7 @@ function handleError(methodName, xhr, status, err) {
if (oldError && oldError.connErrorCount) {
errorCount += oldError.connErrorCount;
connectError = 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.';
connectError = 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.';
}
e = {message: connectError, connErrorCount: errorCount};

Просмотреть файл

@@ -98,6 +98,7 @@ module.exports = {
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
POST_TYPE_JOIN_LEAVE: 'join_leave',
RESERVED_TEAM_NAMES: [
'www',
'web',
@@ -132,6 +133,7 @@ module.exports = {
OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
UPDATE_TYPING_MS: 5000,
THEMES: {
default: {
type: 'Mattermost',
@@ -300,6 +302,13 @@ module.exports = {
uiName: 'Mention Highlight Link'
}
],
CODE_THEMES: {
github: 'GitHub',
solarized_light: 'Solarized light',
monokai: 'Monokai',
solarized_dark: 'Solarized Dark'
},
DEFAULT_CODE_THEME: 'github',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings'
@@ -313,5 +322,30 @@ module.exports = {
ENTER: 13,
ESCAPE: 27,
SPACE: 32
},
HighlightedLanguages: {
diff: 'Diff',
apache: 'Apache',
makefile: 'Makefile',
http: 'HTTP',
json: 'JSON',
markdown: 'Markdown',
javascript: 'JavaScript',
css: 'CSS',
nginx: 'nginx',
objectivec: 'Objective-C',
python: 'Python',
xml: 'XML',
perl: 'Perl',
bash: 'Bash',
php: 'PHP',
coffeescript: 'CoffeeScript',
cs: 'C#',
cpp: 'C++',
sql: 'SQL',
go: 'Go',
ruby: 'Ruby',
java: 'Java',
ini: 'ini'
}
};

Просмотреть файл

@@ -6,6 +6,34 @@ const Utils = require('./utils.jsx');
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;
export class MattermostMarkdownRenderer extends marked.Renderer {
constructor(options, formattingOptions = {}) {
super(options);
@@ -15,6 +43,43 @@ export 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) {
if (!language || highlightJs.listLanguages().indexOf(language) < 0) {
let parsed = super.code(code, language);
return '<code class="hljs">' + $(parsed).text() + '</code>';
}
let parsed = highlightJs.highlight(language, code);
return '<div class="post-body--code">' +
'<span class="post-body--code__language">' + HighlightedLanguages[language] + '</span>' +
'<code style="white-space: pre;" class="hljs">' + parsed.value + '</code>' +
'</div>';
}
br() {

Просмотреть файл

@@ -91,6 +91,7 @@ export function sanitizeHtml(text) {
return output;
}
// Convert URLs into tokens
function autolinkUrls(text, tokens) {
function replaceUrlWithToken(autolinker, match) {
const linkText = match.getMatchedText();
@@ -132,27 +133,61 @@ function autolinkUrls(text, tokens) {
}
function autolinkAtMentions(text, tokens) {
let output = text;
// Return true if provided character is punctuation
function isPunctuation(character) {
const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
return re.test(character);
}
// Test if provided text needs to be highlighted, special mention or current user
function mentionExists(u) {
return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u));
}
function addToken(username, mention, extraText) {
const index = tokens.size;
const alias = `MM_ATMENTION${index}`;
tokens.set(alias, {
value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`,
originalText: mention,
extraText
});
return alias;
}
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
});
let usernameLower = username.toLowerCase();
if (mentionExists(usernameLower)) {
// Exact match
const alias = addToken(usernameLower, mention, '');
return prefix + alias;
}
// Not an exact match, attempt to truncate any punctuation to see if we can find a user
const originalUsername = usernameLower;
for (let c = usernameLower.length; c > 0; c--) {
if (isPunctuation(usernameLower[c - 1])) {
usernameLower = usernameLower.substring(0, c - 1);
if (mentionExists(usernameLower)) {
const extraText = originalUsername.substr(c - 1);
const alias = addToken(usernameLower, '@' + usernameLower, extraText);
return prefix + alias;
}
} else {
// If the last character is not punctuation, no point in going any further
break;
}
}
return fullMatch;
}
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken);
let output = text;
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
return output;
}
@@ -169,10 +204,9 @@ function highlightCurrentMentions(text, tokens) {
const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
value: `<span class='mention-highlight'>${alias}</span>`,
value: `<span class='mention-highlight'>${alias}</span>` + token.extraText,
originalText: token.originalText
});
output = output.replace(alias, newAlias);
}
}
@@ -246,7 +280,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
var newTokens = new Map();
for (const [alias, token] of tokens) {
if (token.originalText === searchTerm) {
if (token.originalText.indexOf(searchTerm.replace(/\*$/, '')) > -1) {
const index = tokens.size + newTokens.size;
const newAlias = `MM_SEARCHTERM${index}`;
@@ -276,7 +310,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
return prefix + alias;
}
return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken);
return output.replace(new RegExp(`()(${searchTerm})`, 'gi'), replaceSearchTermWithToken);
}
function replaceTokens(text, tokens) {

Просмотреть файл

@@ -20,6 +20,7 @@ export function isEmail(email) {
export function cleanUpUrlable(input) {
var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
cleaned = cleaned.replace(/-{2,}/, '-');
cleaned = cleaned.replace(/^\-+/, '');
cleaned = cleaned.replace(/\-+$/, '');
return cleaned;
@@ -402,6 +403,11 @@ export function toTitleCase(str) {
}
export function applyTheme(theme) {
if (!theme.codeTheme) {
theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
updateCodeTheme(theme.codeTheme);
if (theme.sidebarBg) {
changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
}
@@ -588,6 +594,27 @@ export function rgb2hex(rgbIn) {
return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
export function updateCodeTheme(theme) {
const path = '/static/css/highlight/' + theme + '.css';
const $link = $('link.code_theme');
if (path !== $link.attr('href')) {
changeCss('code.hljs', 'visibility: hidden');
var xmlHTTP = new XMLHttpRequest();
xmlHTTP.open('GET', path, true);
xmlHTTP.onload = function onLoad() {
$link.attr('href', path);
if (isBrowserFirefox()) {
$link.one('load', () => {
changeCss('code.hljs', 'visibility: visible');
});
} else {
changeCss('code.hljs', 'visibility: visible');
}
};
xmlHTTP.send();
}
}
export function placeCaretAtEnd(el) {
el.focus();
if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {

Просмотреть файл

@@ -466,6 +466,22 @@ body.ios {
white-space: nowrap;
cursor: pointer;
}
.post-body--code {
font-size: .97em;
position:relative;
.post-body--code__language {
position: absolute;
right: 0;
background: #fff;
cursor: default;
padding: 0.3em 0.5em 0.1em;
border-bottom-left-radius: 4px;
@include opacity(.3);
}
code {
white-space: pre;
}
}
}
.create-reply-form-wrap {
width: 100%;

1
web/static/css/highlight Символическая ссылка
Просмотреть файл

@@ -0,0 +1 @@
../../react/node_modules/highlight.js/styles/

Двоичные данные
web/static/images/themes/code_themes/github.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.4 KiB

Двоичные данные
web/static/images/themes/code_themes/monokai.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.1 KiB

Двоичные данные
web/static/images/themes/code_themes/solarized_dark.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.0 KiB

Двоичные данные
web/static/images/themes/code_themes/solarized_light.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.7 KiB

Просмотреть файл

@@ -24,6 +24,7 @@
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/google-fonts.css">
<link rel="stylesheet" class="code_theme" href="">
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">

Просмотреть файл

@@ -969,20 +969,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var props map[string]string
var parsedRequest *model.IncomingWebhookRequest
if r.Header.Get("Content-Type") == "application/json" {
props = model.MapFromJson(r.Body)
parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
} else {
props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
}
text := props["text"]
text := parsedRequest.Text
if len(text) == 0 {
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
return
}
channelName := props["channel"]
channelName := parsedRequest.ChannelName
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
@@ -1012,8 +1012,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
}
overrideUsername := props["username"]
overrideIconUrl := props["icon_url"]
overrideUsername := parsedRequest.Username
overrideIconUrl := parsedRequest.IconURL
if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)