Этот коммит содержится в:
Asaad Mahmood
2015-10-27 20:15:33 +05:00
родитель 68b02ffb9e 50eb3d9fe4
Коммит 179c4ea684
65 изменённых файлов: 1882 добавлений и 396 удалений

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

@@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component {
var inputs = [];
inputs.push(
<div>
<div key='channel-notification-level-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={notifyActive[0]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
>
/>
{`Global default (${globalNotifyLevelName})`}
</input>
</label>
<br/>
</div>
@@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
>
/>
{'For all activity'}
</input>
</label>
<br/>
</div>
@@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
>
/>
{'Only for mentions'}
</input>
</label>
<br/>
</div>
@@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
/>
{'Never'}
</input>
</label>
</div>
</div>
@@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component {
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
<div>
<div key='channel-notification-unread-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
>
/>
{'For all unread messages'}
</input>
</label>
<br />
</div>
@@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
>
/>
{'Only for mentions'}
</input>
</label>
<br />
</div>
@@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component {
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Close</span>
<span className='sr-only'>{'Close'}</span>
</button>
<h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
</div>

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

@@ -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});
}

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

@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
if (this.state.selectedMention === index) {
isFocused = 'mentions-focus';
}
if (!users[i].secondary_text) {
users[i].secondary_text = Utils.getFullName(users[i]);
}
mentions[index] = (
<Mention
key={'mention_key_' + index}
ref={'mention' + index}
username={users[i].username}
secondary_text={Utils.getFullName(users[i])}
secondary_text={users[i].secondary_text}
id={users[i].id}
listId={index}
isFocused={isFocused}

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

@@ -1,13 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const AsyncClient = require('../utils/async_client.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const Constants = require('../utils/constants.jsx');
const Client = require('../utils/client.jsx');
const Modal = ReactBootstrap.Modal;
const PreferenceStore = require('../stores/preference_store.jsx');
const TeamStore = require('../stores/team_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
@@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component {
}
handleShowDirectChannel(teammate, e) {
e.preventDefault();
if (this.state.loadingDMChannel !== -1) {
return;
}
e.preventDefault();
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
let channel = ChannelStore.getByName(channelName);
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
AsyncClient.savePreferences([preference]);
if (channel) {
Utils.switchChannel(channel);
this.handleHide();
} else {
this.setState({loadingDMChannel: teammate.id});
channel = {
name: channelName,
last_post_at: 0,
total_msg_count: 0,
type: 'D',
display_name: teammate.username,
teammate_id: teammate.id,
status: UserStore.getStatus(teammate.id)
};
Client.createDirectChannel(
channel,
teammate.id,
(data) => {
this.setState({loadingDMChannel: -1});
AsyncClient.getChannel(data.id);
Utils.switchChannel(data);
this.handleHide();
},
() => {
this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
}
);
}
this.setState({loadingDMChannel: teammate.id});
Utils.openDirectChannelToUser(
teammate,
(channel) => {
Utils.switchChannel(channel);
this.setState({loadingDMChannel: -1});
this.handleHide();
},
() => {
this.setState({loadingDMChannel: -1});
}
);
}
handleUserChange() {
@@ -169,7 +135,7 @@ export default class MoreDirectChannels extends React.Component {
}
return (
<tr>
<tr key={'direct-channel-row-user' + user.id}>
<td
key={user.id}
className='direct-channel'

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

@@ -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>

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

@@ -3,9 +3,23 @@
var UserStore = require('../stores/user_store.jsx');
var Popover = ReactBootstrap.Popover;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
var Overlay = ReactBootstrap.Overlay;
const Utils = require('../utils/utils.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
export default class PopoverListMembers extends React.Component {
constructor(props) {
super(props);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.closePopover = this.closePopover.bind(this);
}
componentWillMount() {
this.setState({showPopover: false});
}
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
@@ -27,12 +41,36 @@ export default class PopoverListMembers extends React.Component {
}
};
}
handleShowDirectChannel(teammate, e) {
e.preventDefault();
Utils.openDirectChannelToUser(
teammate,
(channel, channelAlreadyExisted) => {
Utils.switchChannel(channel);
if (channelAlreadyExisted) {
this.closePopover();
}
},
() => {
this.closePopover();
}
);
}
closePopover() {
this.setState({showPopover: false});
}
render() {
let popoverHtml = [];
let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
const ch = ChannelStore.getCurrent();
if (members && teamMembers) {
members.sort((a, b) => {
@@ -40,13 +78,74 @@ export default class PopoverListMembers extends React.Component {
});
members.forEach((m, i) => {
const details = [];
const fullName = Utils.getFullName(m);
if (fullName) {
details.push(
<span
key={`${m.id}__full-name`}
className='full-name'
>
{fullName}
</span>
);
}
if (m.nickname) {
const separator = fullName ? ' - ' : '';
details.push(
<span
key={`${m.nickname}__nickname`}
>
{separator + m.nickname}
</span>
);
}
let button = '';
if (currentUserId !== m.id && ch.type !== 'D') {
button = (
<button
type='button'
className='btn btn-primary btn-message'
onClick={(e) => this.handleShowDirectChannel(m, e)}
>
{'Message'}
</button>
);
}
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
className='text--nowrap'
key={'popover-member-' + i}
>
{m.username}
<img
className='profile-img pull-left'
width='38'
height='38'
src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
/>
<div className='pull-left'>
<div
className='more-name'
>
{m.username}
</div>
<div
className='more-description'
>
{details}
</div>
</div>
<div
className='pull-right profile-action'
>
{button}
</div>
</div>
);
count++;
@@ -61,29 +160,37 @@ export default class PopoverListMembers extends React.Component {
}
return (
<OverlayTrigger
trigger='click'
placement='bottom'
rootClose={true}
overlay={
<div>
<div
id='member_popover'
ref='member_popover_target'
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
>
<div>
{countText}
<span
className='fa fa-user'
aria-hidden='true'
/>
</div>
</div>
<Overlay
rootClose={true}
onHide={this.closePopover}
show={this.state.showPopover}
target={() => this.state.popoverTarget}
placement='bottom'
>
<Popover
title='Members'
id='member-list-popover'
>
{popoverHtml}
<div>
{popoverHtml}
</div>
</Popover>
}
>
<div id='member_popover'>
<div>
{countText}
<span
className='fa fa-user'
aria-hidden='true'
/>
</div>
</Overlay>
</div>
</OverlayTrigger>
);
}
}

249
web/react/components/search_autocomplete.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,249 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const ChannelStore = require('../stores/channel_store.jsx');
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
export default class SearchAutocomplete extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.completeWord = this.completeWord.bind(this);
this.updateSuggestions = this.updateSuggestions.bind(this);
this.state = {
show: false,
mode: '',
filter: '',
selection: 0,
suggestions: new Map()
};
}
componentDidMount() {
$(document).on('click', this.handleDocumentClick);
}
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
handleClick(value) {
this.completeWord(value);
}
handleDocumentClick(e) {
const container = $(ReactDOM.findDOMNode(this.refs.container));
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
this.setState({
show: false
});
}
}
handleInputChange(textbox, text) {
const caret = Utils.getCaretPosition(textbox);
const preText = text.substring(0, caret);
let mode = '';
let filter = '';
for (const [modeForPattern, pattern] of patterns) {
const result = pattern.exec(preText);
if (result) {
mode = modeForPattern;
filter = result[1];
break;
}
}
if (mode !== this.state.mode || filter !== this.state.filter) {
this.updateSuggestions(mode, filter);
}
this.setState({
mode,
filter,
show: mode || filter
});
}
handleKeyDown(e) {
if (!this.state.show || this.state.suggestions.length === 0) {
return;
}
if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
e.preventDefault();
let selection = this.state.selection;
if (e.which === KeyCodes.UP) {
selection -= 1;
} else {
selection += 1;
}
if (selection >= 0 && selection < this.state.suggestions.length) {
this.setState({
selection
});
}
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
e.preventDefault();
this.completeSelectedWord();
}
}
completeSelectedWord() {
if (this.state.mode === 'channels') {
this.completeWord(this.state.suggestions[this.state.selection].name);
} else if (this.state.mode === 'users') {
this.completeWord(this.state.suggestions[this.state.selection].username);
}
}
completeWord(value) {
// add a space so that anything else typed doesn't interfere with the search flag
this.props.completeWord(this.state.filter, value + ' ');
this.setState({
show: false,
mode: '',
filter: '',
selection: 0
});
}
updateSuggestions(mode, filter) {
let suggestions = [];
if (mode === 'channels') {
let channels = ChannelStore.getAll();
if (filter) {
channels = channels.filter((channel) => channel.name.startsWith(filter));
}
channels.sort((a, b) => a.name.localeCompare(b.name));
suggestions = channels;
} else if (mode === 'users') {
let users = UserStore.getActiveOnlyProfileList();
if (filter) {
users = users.filter((user) => user.username.startsWith(filter));
}
users.sort((a, b) => a.username.localeCompare(b.username));
suggestions = users;
}
let selection = this.state.selection;
// keep the same user/channel selected if it's still visible as a suggestion
if (selection > 0 && this.state.suggestions.length > 0) {
// we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
const currentSelectionId = this.state.suggestions[selection].id;
let found = false;
for (let i = 0; i < suggestions.length; i++) {
if (suggestions[i].id === currentSelectionId) {
selection = i;
found = true;
break;
}
}
if (!found) {
selection = 0;
}
} else {
selection = 0;
}
this.setState({
suggestions,
selection
});
}
render() {
if (!this.state.show || this.state.suggestions.length === 0) {
return null;
}
let suggestions = [];
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
let className = 'search-autocomplete__channel';
if (this.state.selection === index) {
className += ' selected';
}
return (
<div
key={channel.name}
ref={channel.name}
onClick={this.handleClick.bind(this, channel.name)}
className={className}
>
{channel.name}
</div>
);
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
let className = 'search-autocomplete__user';
if (this.state.selection === index) {
className += ' selected';
}
return (
<div
key={user.username}
ref={user.username}
onClick={this.handleClick.bind(this, user.username)}
className={className}
>
<img
className='profile-img'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
</div>
);
});
}
return (
<div
ref='container'
className='search-autocomplete'
>
{suggestions}
</div>
);
}
}
SearchAutocomplete.propTypes = {
completeWord: React.PropTypes.func.isRequired
};

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

@@ -9,6 +9,7 @@ var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var Popover = ReactBootstrap.Popover;
var SearchAutocomplete = require('./search_autocomplete.jsx');
export default class SearchBar extends React.Component {
constructor() {
@@ -16,11 +17,13 @@ export default class SearchBar extends React.Component {
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.completeWord = this.completeWord.bind(this);
const state = this.getSearchTermStateFromStores();
state.focused = false;
@@ -74,11 +77,18 @@ export default class SearchBar extends React.Component {
results: null
});
}
handleKeyDown(e) {
if (this.refs.autocomplete) {
this.refs.autocomplete.handleKeyDown(e);
}
}
handleUserInput(e) {
var term = e.target.value;
PostStore.storeSearchTerm(term);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
}
handleMouseInput(e) {
e.preventDefault();
@@ -95,9 +105,16 @@ 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,
function success(data) {
searchTerms,
(data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {
ReactDOM.findDOMNode(this.refs.search).value = '';
@@ -108,11 +125,11 @@ export default class SearchBar extends React.Component {
results: data,
is_mention_search: isMentionSearch
});
}.bind(this),
function error(err) {
},
(err) => {
this.setState({isSearching: false});
AsyncClient.dispatchError(err, 'search');
}.bind(this)
}
);
}
}
@@ -120,6 +137,24 @@ export default class SearchBar extends React.Component {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
}
completeWord(partialWord, word) {
const textbox = ReactDOM.findDOMNode(this.refs.search);
let text = textbox.value;
const caret = utils.getCaretPosition(textbox);
const preText = text.substring(0, caret - partialWord.length);
const postText = text.substring(caret);
text = preText + word + postText;
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
PostStore.storeSearchTerm(text);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}
render() {
var isSearching = null;
if (this.state.isSearching) {
@@ -143,12 +178,13 @@ export default class SearchBar extends React.Component {
className='search__clear'
onClick={this.clearFocus}
>
Cancel
{'Cancel'}
</span>
<form
role='form'
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{overflow: 'visible'}}
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -160,10 +196,16 @@ export default class SearchBar extends React.Component {
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
ref='autocomplete'
completeWord={this.completeWord}
/>
<Popover
id='searchbar-help-popup'
placement='bottom'
className={helpClass}
>

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

@@ -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;

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

@@ -15,11 +15,6 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.state = {
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true'
};
if (!this.state.emailEnabled) {
this.props.state.wizard = 'username';
this.props.updateParent(this.props.state);
}
}
submitBack(e) {
e.preventDefault();

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

@@ -54,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component {
if (data) {
this.setState({nameError: 'This URL is unavailable. Please try another.'});
} else {
this.props.state.wizard = 'send_invites';
if (global.window.mm_config.SendEmailNotifications === 'true') {
this.props.state.wizard = 'send_invites';
} else {
this.props.state.wizard = 'username';
}
this.props.state.team.type = 'O';
this.props.state.team.name = name;

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

@@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component {
return (
<div>
<p>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h3 className='sub-heading'>Welcome to:</h3>
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
</p>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h3 className='sub-heading'>Welcome to:</h3>
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
<p className='margin--less'>Let's set up your new team</p>
<p>
<div>
Please confirm your email address:<br />
<div className='inner__content'>
<div className='block--gray'>{this.props.state.team.email}</div>
</div>
</p>
</div>
<p className='margin--extra color--light'>
Your account will administer the new team site. <br />
You can add other administrators later.

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

@@ -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)
);