Merge branch 'master' into PLT-25
Этот коммит содержится в:
@@ -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;
|
||||
|
||||
55
web/react/components/user_settings/code_theme_chooser.jsx
Обычный файл
55
web/react/components/user_settings/code_theme_chooser.jsx
Обычный файл
@@ -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)
|
||||
);
|
||||
|
||||
Ссылка в новой задаче
Block a user