Merge branch 'master' of https://github.com/mattermost/platform into ui-improvements
Этот коммит содержится в:
@@ -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'>×</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
Обычный файл
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.
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter {
|
||||
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
|
||||
this.getProfiles = this.getProfiles.bind(this);
|
||||
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
|
||||
this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
|
||||
this.saveProfile = this.saveProfile.bind(this);
|
||||
this.setSessions = this.setSessions.bind(this);
|
||||
this.getSessions = this.getSessions.bind(this);
|
||||
@@ -215,6 +216,19 @@ class UserStoreClass extends EventEmitter {
|
||||
return active;
|
||||
}
|
||||
|
||||
getActiveOnlyProfileList() {
|
||||
const profileMap = this.getActiveOnlyProfiles();
|
||||
const profiles = [];
|
||||
|
||||
for (const id in profileMap) {
|
||||
if (profileMap.hasOwnProperty(id)) {
|
||||
profiles.push(profileMap[id]);
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
saveProfile(profile) {
|
||||
var ps = this.getProfiles();
|
||||
ps[profile.id] = profile;
|
||||
|
||||
@@ -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'
|
||||
@@ -311,6 +320,32 @@ module.exports = {
|
||||
RIGHT: 39,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27
|
||||
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() {
|
||||
@@ -56,8 +121,11 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
paragraph(text) {
|
||||
let outText = text;
|
||||
|
||||
// required so markdown does not strip '_' from @user_names
|
||||
outText = TextFormatting.doFormatMentions(text);
|
||||
|
||||
if (!('emoticons' in this.options) || this.options.emoticon) {
|
||||
outText = TextFormatting.doFormatEmoticons(text);
|
||||
outText = TextFormatting.doFormatEmoticons(outText);
|
||||
}
|
||||
|
||||
if (this.formattingOptions.singleline) {
|
||||
@@ -71,7 +139,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
|
||||
text(text) {
|
||||
return TextFormatting.doFormatText(text, this.formattingOptions);
|
||||
text(txt) {
|
||||
return TextFormatting.doFormatText(txt, this.formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ export function doFormatText(text, options) {
|
||||
const tokens = new Map();
|
||||
|
||||
// replace important words and phrases with tokens
|
||||
output = autolinkUrls(output, tokens);
|
||||
output = autolinkAtMentions(output, tokens);
|
||||
output = autolinkUrls(output, tokens);
|
||||
output = autolinkHashtags(output, tokens);
|
||||
|
||||
if (!('emoticons' in options) || options.emoticon) {
|
||||
@@ -78,6 +78,13 @@ export function doFormatEmoticons(text) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function doFormatMentions(text) {
|
||||
const tokens = new Map();
|
||||
let output = autolinkAtMentions(text, tokens);
|
||||
output = replaceTokens(output, tokens);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function sanitizeHtml(text) {
|
||||
let output = text;
|
||||
|
||||
@@ -91,6 +98,7 @@ export function sanitizeHtml(text) {
|
||||
return output;
|
||||
}
|
||||
|
||||
// Convert URLs into tokens
|
||||
function autolinkUrls(text, tokens) {
|
||||
function replaceUrlWithToken(autolinker, match) {
|
||||
const linkText = match.getMatchedText();
|
||||
@@ -132,26 +140,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 +212,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 +288,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 +318,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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ var PreferenceStore = require('../stores/preference_store.jsx');
|
||||
var TeamStore = require('../stores/team_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
var Client = require('./client.jsx');
|
||||
var AsyncClient = require('./async_client.jsx');
|
||||
var client = require('./client.jsx');
|
||||
var Autolinker = require('autolinker');
|
||||
@@ -20,6 +21,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 +404,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 +595,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') {
|
||||
@@ -982,3 +1010,44 @@ export function windowWidth() {
|
||||
export function windowHeight() {
|
||||
return $(window).height();
|
||||
}
|
||||
|
||||
export function openDirectChannelToUser(user, successCb, errorCb) {
|
||||
const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
|
||||
let channel = ChannelStore.getByName(channelName);
|
||||
|
||||
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
||||
AsyncClient.savePreferences([preference]);
|
||||
|
||||
if (channel) {
|
||||
if ($.isFunction(successCb)) {
|
||||
successCb(channel, true);
|
||||
}
|
||||
} else {
|
||||
channel = {
|
||||
name: channelName,
|
||||
last_post_at: 0,
|
||||
total_msg_count: 0,
|
||||
type: 'D',
|
||||
display_name: user.username,
|
||||
teammate_id: user.id,
|
||||
status: UserStore.getStatus(user.id)
|
||||
};
|
||||
|
||||
Client.createDirectChannel(
|
||||
channel,
|
||||
user.id,
|
||||
(data) => {
|
||||
AsyncClient.getChannel(data.id);
|
||||
if ($.isFunction(successCb)) {
|
||||
successCb(data, false);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
|
||||
if ($.isFunction(errorCb)) {
|
||||
errorCb();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user