first commit
Этот коммит содержится в:
249
web/react/components/channel_header.jsx
Обычный файл
249
web/react/components/channel_header.jsx
Обычный файл
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var UserProfile = require( './user_profile.jsx' );
|
||||
var NavbarSearchBox =require('./search_bar.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var MessageWrapper = require('./message_wrapper.jsx');
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getExtraInfoStateFromStores() {
|
||||
return {
|
||||
extra_info: ChannelStore.getCurrentExtraInfo()
|
||||
};
|
||||
}
|
||||
|
||||
var ExtraMembers = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
|
||||
var originalLeave = $.fn.popover.Constructor.prototype.leave;
|
||||
$.fn.popover.Constructor.prototype.leave = function(obj) {
|
||||
var self = obj instanceof this.constructor ? obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type);
|
||||
originalLeave.call(this, obj);
|
||||
|
||||
if (obj.currentTarget && self.$tip) {
|
||||
self.$tip.one('mouseenter', function() {
|
||||
clearTimeout(self.timeout);
|
||||
self.$tip.one('mouseleave', function() {
|
||||
$.fn.popover.Constructor.prototype.leave.call(self, self);
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
$("#member_popover").popover({placement : 'bottom', trigger: 'click', html: true});
|
||||
$('body').on('click', function (e) {
|
||||
if ($(e.target.parentNode.parentNode)[0] !== $("#member_popover")[0] && $(e.target).parents('.popover.in').length === 0) {
|
||||
$("#member_popover").popover('hide');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getExtraInfoStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getExtraInfoStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var count = this.state.extra_info.members.length == 0 ? "-" : this.state.extra_info.members.length;
|
||||
count = this.state.extra_info.members.length > 19 ? "20+" : count;
|
||||
var data_content = "";
|
||||
|
||||
this.state.extra_info.members.forEach(function(m) {
|
||||
data_content += "<div style='white-space: nowrap'>" + m.username + "</div>";
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} data-original-title="Members" >
|
||||
<div id="member_tooltip" data-toggle="tooltip" title="View Channel Members">
|
||||
{count} <span className="glyphicon glyphicon-user" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
channel: ChannelStore.getCurrent(),
|
||||
memberChannel: ChannelStore.getCurrentMember(),
|
||||
memberTeam: UserStore.getCurrentUser(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members,
|
||||
search_visible: PostStore.getSearchResults() != null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
PostStore.addSearchChangeListener(this._onChange);
|
||||
UserStore.addChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this._onChange);
|
||||
PostStore.removeSearchChangeListener(this._onChange);
|
||||
UserStore.addChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
$(".channel-header__info .description").popover({placement : 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
handleLeave: function(e) {
|
||||
var self = this;
|
||||
Client.leaveChannel(this.state.channel.id,
|
||||
function(data) {
|
||||
var townsquare = ChannelStore.getByName('town-square');
|
||||
utils.switchChannel(townsquare);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "handleLeave");
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
searchMentions: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var user = UserStore.getCurrentUser();
|
||||
|
||||
var terms = "";
|
||||
if (user.notify_props && user.notify_props.mention_keys) {
|
||||
terms = UserStore.getCurrentMentionKeys().join(' ');
|
||||
}
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH_TERM,
|
||||
term: terms,
|
||||
do_search: false
|
||||
});
|
||||
|
||||
Client.search(
|
||||
terms,
|
||||
function(data) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: data,
|
||||
is_mention_search: true
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
dispatchError(err, "search");
|
||||
}
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
|
||||
if (this.state.channel == null) {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
var description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true});
|
||||
var popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>);
|
||||
var channelTitle = "";
|
||||
var channelName = this.state.channel.name;
|
||||
var currentId = UserStore.getCurrentId();
|
||||
var isAdmin = this.state.memberChannel.roles.indexOf("admin") > -1 || this.state.memberTeam.roles.indexOf("admin") > -1;
|
||||
var searchForm = <th className="search-bar__container"><NavbarSearchBox /></th>;
|
||||
var isDirect = false;
|
||||
|
||||
if (this.state.channel.type === 'O') {
|
||||
channelTitle = this.state.channel.display_name;
|
||||
} else if (this.state.channel.type === 'P') {
|
||||
channelTitle = this.state.channel.display_name;
|
||||
} else if (this.state.channel.type === 'D') {
|
||||
isDirect = true;
|
||||
if (this.state.users.length > 1) {
|
||||
if (this.state.users[0].id === UserStore.getCurrentId()) {
|
||||
channelTitle = <UserProfile userId={this.state.users[1].id} overwriteName={this.state.users[1].full_name ? this.state.users[1].full_name : this.state.users[1].username} />;
|
||||
} else {
|
||||
channelTitle = <UserProfile userId={this.state.users[0].id} overwriteName={this.state.users[0].full_name ? this.state.users[0].full_name : this.state.users[0].username} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="channel-header alt">
|
||||
<tr>
|
||||
<th>
|
||||
{ !isDirect ?
|
||||
<div className="channel-header__info">
|
||||
<div className="dropdown">
|
||||
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true">
|
||||
<strong className="heading">{channelTitle} </strong>
|
||||
<span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown">
|
||||
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={this.state.channel.id} href="#">View Info</a></li>
|
||||
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Invite Members</a></li>
|
||||
{ isAdmin ?
|
||||
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li>
|
||||
: ""
|
||||
}
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li>
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li>
|
||||
{ isAdmin && channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li>
|
||||
: ""
|
||||
}
|
||||
{ isAdmin && channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li>
|
||||
: ""
|
||||
}
|
||||
{ channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li>
|
||||
: ""
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div data-toggle="popover" data-content={popoverContent} className="description">{description}</div>
|
||||
</div>
|
||||
:
|
||||
<a href="#"><strong className="heading">{channelTitle}</strong></a>
|
||||
}
|
||||
</th>
|
||||
<th><ExtraMembers channelId={this.state.channel.id} /></th>
|
||||
{ searchForm }
|
||||
<th>
|
||||
<div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}>
|
||||
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_right_dropdown" data-toggle="dropdown" aria-expanded="true">
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_right_dropdown" style={{"left": "-150px"}}>
|
||||
<li role="presentation"><a role="menuitem" href="#" onClick={this.searchMentions}>Recent Mentions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
50
web/react/components/channel_info_modal.jsx
Обычный файл
50
web/react/components/channel_info_modal.jsx
Обычный файл
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
if(this.refs.modal) {
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ channel_id: $(button).attr('data-channelid') });
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { channel_id: ChannelStore.getCurrentId() };
|
||||
},
|
||||
render: function() {
|
||||
var channel = ChannelStore.get(this.state.channel_id);
|
||||
|
||||
if (!channel) {
|
||||
channel = {};
|
||||
channel.display_name = "No Channel Found";
|
||||
channel.name = "No Channel Found";
|
||||
channel.id = "No Channel Found";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="channel_info" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" id="myModalLabel">{channel.display_name}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p><strong>Channel Name: </strong>{channel.display_name}</p>
|
||||
<p><strong>Channel Handle: </strong>{channel.name}</p>
|
||||
<p><strong>Channel ID: </strong>{channel.id}</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
157
web/react/components/channel_invite_modal.jsx
Обычный файл
157
web/react/components/channel_invite_modal.jsx
Обычный файл
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var MemberList = require('./member_list.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
var users = UserStore.getActiveOnlyProfiles();
|
||||
var member_list = ChannelStore.getCurrentExtraInfo().members;
|
||||
|
||||
var nonmember_list = [];
|
||||
for (var id in users) {
|
||||
var found = false;
|
||||
for (var i = 0; i < member_list.length; i++) {
|
||||
if (member_list[i].id === id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
nonmember_list.push(users[id]);
|
||||
}
|
||||
}
|
||||
|
||||
member_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
nonmember_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var channel_name = ChannelStore.getCurrent() ? ChannelStore.getCurrent().display_name : "";
|
||||
|
||||
return {
|
||||
nonmember_list: nonmember_list,
|
||||
member_list: member_list,
|
||||
channel_name: channel_name
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) {
|
||||
self.setState({ render_members: false });
|
||||
});
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
self.setState({ render_members: true });
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var new_state = getStateFromStores();
|
||||
if (!utils.areStatesEqual(this.state, new_state)) {
|
||||
this.setState(new_state);
|
||||
}
|
||||
},
|
||||
handleInvite: function(user_id) {
|
||||
// Make sure the user isn't already a member of the channel
|
||||
var member_list = this.state.member_list;
|
||||
for (var i = 0; i < member_list; i++) {
|
||||
if (member_list[i].id === user_id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data['user_id'] = user_id;
|
||||
|
||||
client.addChannelMember(ChannelStore.getCurrentId(), data,
|
||||
function(data) {
|
||||
var nonmember_list = this.state.nonmember_list;
|
||||
var new_member;
|
||||
for (var i = 0; i < nonmember_list.length; i++) {
|
||||
if (user_id === nonmember_list[i].id) {
|
||||
nonmember_list[i].invited = true;
|
||||
new_member = nonmember_list[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (new_member) {
|
||||
member_list.push(new_member);
|
||||
member_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ invite_error: null, member_list: member_list, nonmember_list: nonmember_list });
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ invite_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var invite_error = this.state.invite_error ? <label className='has-error control-label'>{this.state.invite_error}</label> : null;
|
||||
|
||||
var currentMember = ChannelStore.getCurrentMember();
|
||||
var isAdmin = false;
|
||||
if (currentMember) {
|
||||
isAdmin = currentMember.roles.indexOf("admin") > -1 || UserStore.getCurrentUser().roles.indexOf("admin") > -1;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="channel_invite" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title">Add New Members to {this.state.channel_name}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{ invite_error }
|
||||
{ this.state.render_members ?
|
||||
<MemberList
|
||||
memberList={this.state.nonmember_list}
|
||||
isAdmin={isAdmin}
|
||||
handleInvite={this.handleInvite}
|
||||
/>
|
||||
: "" }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
62
web/react/components/channel_loader.jsx
Обычный файл
62
web/react/components/channel_loader.jsx
Обычный файл
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
/* This is a special React control with the sole purpose of making all the AsyncClient calls
|
||||
to the server on page load. This is to prevent other React controls from spamming
|
||||
AsyncClient with requests. */
|
||||
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
/* Start initial aysnc loads */
|
||||
AsyncClient.getMe();
|
||||
AsyncClient.getPosts(true);
|
||||
AsyncClient.getChannels(true, true);
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
AsyncClient.findTeams();
|
||||
AsyncClient.getStatuses();
|
||||
/* End of async loads */
|
||||
|
||||
|
||||
/* Start interval functions */
|
||||
setInterval(function(){AsyncClient.getStatuses();}, 30000);
|
||||
/* End interval functions */
|
||||
|
||||
|
||||
/* Start device tracking setup */
|
||||
var iOS = /(iPad|iPhone|iPod)/g.test( navigator.userAgent );
|
||||
if (iOS) {
|
||||
$("body").addClass("ios");
|
||||
}
|
||||
/* End device tracking setup */
|
||||
|
||||
|
||||
/* Start window active tracking setup */
|
||||
window.isActive = true;
|
||||
|
||||
$(window).focus(function() {
|
||||
AsyncClient.updateLastViewedAt();
|
||||
window.isActive = true;
|
||||
});
|
||||
|
||||
$(window).blur(function() {
|
||||
window.isActive = false;
|
||||
});
|
||||
/* End window active tracking setup */
|
||||
|
||||
/* Start global change listeners setup */
|
||||
SocketStore.addChangeListener(this._onSocketChange);
|
||||
/* End global change listeners setup */
|
||||
},
|
||||
_onSocketChange: function(msg) {
|
||||
if (msg && msg.user_id) {
|
||||
UserStore.setStatus(msg.user_id, "online");
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
return <div/>;
|
||||
}
|
||||
});
|
||||
154
web/react/components/channel_members.jsx
Обычный файл
154
web/react/components/channel_members.jsx
Обычный файл
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var MemberList = require('./member_list.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
var users = UserStore.getActiveOnlyProfiles();
|
||||
var member_list = ChannelStore.getCurrentExtraInfo().members;
|
||||
|
||||
var nonmember_list = [];
|
||||
for (var id in users) {
|
||||
var found = false;
|
||||
for (var i = 0; i < member_list.length; i++) {
|
||||
if (member_list[i].id === id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
nonmember_list.push(users[id]);
|
||||
}
|
||||
}
|
||||
|
||||
member_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
nonmember_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var channel_name = ChannelStore.getCurrent() ? ChannelStore.getCurrent().display_name : "";
|
||||
|
||||
return {
|
||||
nonmember_list: nonmember_list,
|
||||
member_list: member_list,
|
||||
channel_name: channel_name
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) {
|
||||
self.setState({ render_members: false });
|
||||
});
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
self.setState({ render_members: true });
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this._onChange);
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var new_state = getStateFromStores();
|
||||
if (!utils.areStatesEqual(this.state, new_state)) {
|
||||
this.setState(new_state);
|
||||
}
|
||||
},
|
||||
handleRemove: function(user_id) {
|
||||
// Make sure the user is a member of the channel
|
||||
var member_list = this.state.member_list;
|
||||
var found = false;
|
||||
for (var i = 0; i < member_list.length; i++) {
|
||||
if (member_list[i].id === user_id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) { return };
|
||||
|
||||
var data = {};
|
||||
data['user_id'] = user_id;
|
||||
|
||||
client.removeChannelMember(ChannelStore.getCurrentId(), data,
|
||||
function(data) {
|
||||
var old_member;
|
||||
for (var i = 0; i < member_list.length; i++) {
|
||||
if (user_id === member_list[i].id) {
|
||||
old_member = member_list[i];
|
||||
member_list.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var nonmember_list = this.state.nonmember_list;
|
||||
if (old_member) {
|
||||
nonmember_list.push(old_member);
|
||||
}
|
||||
|
||||
this.setState({ member_list: member_list, nonmember_list: nonmember_list });
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ invite_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var currentMember = ChannelStore.getCurrentMember();
|
||||
var isAdmin = false;
|
||||
if (currentMember) {
|
||||
isAdmin = currentMember.roles.indexOf("admin") > -1 || UserStore.getCurrentUser().roles.indexOf("admin") > -1;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="channel_members" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title">{this.state.channel_name + " Members"}</h4>
|
||||
<a className="btn btn-md btn-primary" data-toggle="modal" data-target="#channel_invite"><i className="glyphicon glyphicon-envelope"/> Add New Members</a>
|
||||
</div>
|
||||
<div ref="modalBody" className="modal-body">
|
||||
<div className="col-sm-12">
|
||||
<div className="team-member-list">
|
||||
{ this.state.render_members ?
|
||||
<MemberList
|
||||
memberList={this.state.member_list}
|
||||
isAdmin={isAdmin}
|
||||
handleRemove={this.handleRemove}
|
||||
/>
|
||||
: "" }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
120
web/react/components/channel_notifications.jsx
Обычный файл
120
web/react/components/channel_notifications.jsx
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
var channel_id = button.dataset.channelid;
|
||||
|
||||
var notifyLevel = ChannelStore.getMember(channel_id).notify_level;
|
||||
self.setState({ notify_level: notifyLevel, title: button.dataset.title, channel_id: channel_id });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { notify_level: "", title: "", channel_id: "" };
|
||||
},
|
||||
handleUpdate: function(e) {
|
||||
var channel_id = this.state.channel_id;
|
||||
var notify_level = this.state.notify_level;
|
||||
|
||||
var data = {};
|
||||
data["channel_id"] = channel_id;
|
||||
data["user_id"] = UserStore.getCurrentId();
|
||||
data["notify_level"] = this.state.notify_level;
|
||||
|
||||
if (!data["notify_level"] || data["notify_level"].length === 0) return;
|
||||
|
||||
client.updateNotifyLevel(data,
|
||||
function(data) {
|
||||
var member = ChannelStore.getMember(channel_id);
|
||||
member.notify_level = notify_level;
|
||||
ChannelStore.setChannelMember(member);
|
||||
$(this.refs.modal.getDOMNode()).modal('hide');
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleRadioClick: function(notifyLevel) {
|
||||
this.setState({ notify_level: notifyLevel });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
},
|
||||
handleQuietToggle: function() {
|
||||
if (this.state.notify_level === "quiet") {
|
||||
this.setState({ notify_level: "none" });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
} else {
|
||||
this.setState({ notify_level: "quiet" });
|
||||
this.refs.modal.getDOMNode().focus();
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
var allActive = "";
|
||||
var mentionActive = "";
|
||||
var noneActive = "";
|
||||
var quietActive = "";
|
||||
var desktopHidden = "";
|
||||
|
||||
if (this.state.notify_level === "quiet") {
|
||||
desktopHidden = "hidden";
|
||||
quietActive = "active";
|
||||
} else if (this.state.notify_level === "mention") {
|
||||
mentionActive = "active";
|
||||
} else if (this.state.notify_level === "none") {
|
||||
noneActive = "active";
|
||||
} else {
|
||||
allActive = "active";
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="modal fade" id="channel_notifications" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">{"Notification Preferences for " + this.state.title}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className={desktopHidden}>
|
||||
<span>Desktop Notifications</span>
|
||||
<br/>
|
||||
<div className="btn-group" data-toggle="buttons-radio">
|
||||
<button className={"btn btn-default "+allActive} onClick={function(){self.handleRadioClick("all")}}>Any activity (default)</button>
|
||||
<button className={"btn btn-default "+mentionActive} onClick={function(){self.handleRadioClick("mention")}}>Mentions of my name</button>
|
||||
<button className={"btn btn-default "+noneActive} onClick={function(){self.handleRadioClick("none")}}>Nothing</button>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<span>Quiet Mode</span>
|
||||
<br/>
|
||||
<div className="btn-group" data-toggle="buttons-checkbox">
|
||||
<button className={"btn btn-default "+quietActive} onClick={this.handleQuietToggle}>Quiet Mode</button>
|
||||
</div>
|
||||
{ server_error }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-primary" onClick={this.handleUpdate}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
67
web/react/components/command_list.jsx
Обычный файл
67
web/react/components/command_list.jsx
Обычный файл
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function() {
|
||||
return { suggestions: [ ], cmd: "" };
|
||||
},
|
||||
handleClick: function(i) {
|
||||
this.props.addCommand(this.state.suggestions[i].suggestion)
|
||||
this.setState({ suggestions: [ ], cmd: "" });
|
||||
},
|
||||
addFirstCommand: function() {
|
||||
if (this.state.suggestions.length == 0) return;
|
||||
this.handleClick(0);
|
||||
},
|
||||
isEmpty: function() {
|
||||
return this.state.suggestions.length == 0;
|
||||
},
|
||||
getSuggestedCommands: function(cmd) {
|
||||
|
||||
if (cmd == "") {
|
||||
this.setState({ suggestions: [ ], cmd: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd.indexOf("/") != 0) {
|
||||
this.setState({ suggestions: [ ], cmd: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
client.executeCommand(
|
||||
this.props.channelId,
|
||||
cmd,
|
||||
true,
|
||||
function(data) {
|
||||
if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) data.suggestions = [];
|
||||
this.setState({ suggestions: data.suggestions, cmd: cmd });
|
||||
}.bind(this),
|
||||
function(err){
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
if (this.state.suggestions.length == 0) return (<div/>);
|
||||
|
||||
var suggestions = []
|
||||
|
||||
for (var i = 0; i < this.state.suggestions.length; i++) {
|
||||
if (this.state.suggestions[i].suggestion != this.state.cmd) {
|
||||
suggestions.push(
|
||||
<div key={i} className="command-name" onClick={this.handleClick.bind(this, i)}>
|
||||
<div className="pull-left"><strong>{ this.state.suggestions[i].suggestion }</strong></div>
|
||||
<div className="command-desc pull-right">{ this.state.suggestions[i].description }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions*37)+2}}>
|
||||
{ suggestions }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
166
web/react/components/create_comment.jsx
Обычный файл
166
web/react/components/create_comment.jsx
Обычный файл
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient =require('../utils/async_client.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var Textbox = require('./textbox.jsx');
|
||||
var MsgTyping = require('./msg_typing.jsx');
|
||||
var FileUpload = require('./file_upload.jsx');
|
||||
var FilePreview = require('./file_preview.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
lastTime: 0,
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.uploadsInProgress > 0) return;
|
||||
|
||||
if (this.state.submitting) return;
|
||||
|
||||
var post = {}
|
||||
post.filenames = [];
|
||||
|
||||
post.message = this.state.messageText;
|
||||
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.message.length > Constants.CHARACTER_LIMIT) {
|
||||
this.setState({ post_error: 'Comment length must be less than '+Constants.CHARACTER_LIMIT+' characters.' });
|
||||
return;
|
||||
}
|
||||
|
||||
post.channel_id = this.props.channelId;
|
||||
post.root_id = this.props.rootId;
|
||||
post.parent_id = this.props.parentId;
|
||||
post.filenames = this.state.previews;
|
||||
|
||||
this.setState({ submitting: true });
|
||||
|
||||
client.createPost(post, ChannelStore.getCurrent(),
|
||||
function(data) {
|
||||
this.setState({ messageText: '', submitting: false, post_error: null });
|
||||
this.clearPreviews();
|
||||
AsyncClient.getPosts(true, this.props.channelId);
|
||||
|
||||
var channel = ChannelStore.get(this.props.channelId);
|
||||
var member = ChannelStore.getMember(this.props.channelId);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = (new Date).getTime();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
var state = {}
|
||||
state.server_error = err.message;
|
||||
this.setState(state);
|
||||
if (err.message === "Invalid RootId parameter") {
|
||||
if ($('#post_deleted').length > 0) $('#post_deleted').modal('show');
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
commentMsgKeyPress: function(e) {
|
||||
if (e.which == 13 && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
this.refs.textbox.getDOMNode().blur();
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
|
||||
var t = new Date().getTime();
|
||||
if ((t - this.lastTime) > 5000) {
|
||||
SocketStore.sendMessage({channel_id: this.props.channelId, action: "typing", props: {"parent_id": this.props.rootId} });
|
||||
this.lastTime = t;
|
||||
}
|
||||
},
|
||||
handleUserInput: function(messageText) {
|
||||
$(".post-right__scroll").scrollTop($(".post-right__scroll")[0].scrollHeight);
|
||||
$(".post-right__scroll").perfectScrollbar('update');
|
||||
this.setState({messageText: messageText});
|
||||
},
|
||||
handleFileUpload: function(newPreviews) {
|
||||
$(".post-right__scroll").scrollTop($(".post-right__scroll")[0].scrollHeight);
|
||||
$(".post-right__scroll").perfectScrollbar('update');
|
||||
var oldPreviews = this.state.previews;
|
||||
var num = this.state.uploadsInProgress;
|
||||
this.setState({previews: oldPreviews.concat(newPreviews), uploadsInProgress:num-1});
|
||||
},
|
||||
handleUploadError: function(err) {
|
||||
this.setState({ server_error: err });
|
||||
},
|
||||
clearPreviews: function() {
|
||||
this.setState({previews: []});
|
||||
},
|
||||
removePreview: function(filename) {
|
||||
var previews = this.state.previews;
|
||||
for (var i = 0; i < previews.length; i++) {
|
||||
if (previews[i] === filename) {
|
||||
previews.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({previews: previews});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
|
||||
},
|
||||
setUploads: function(val) {
|
||||
var num = this.state.uploadsInProgress + val;
|
||||
this.setState({uploadsInProgress: num});
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
|
||||
|
||||
var preview = <div/>;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress} />
|
||||
);
|
||||
}
|
||||
var limit_previews = ""
|
||||
if (this.state.previews.length > 5) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
|
||||
}
|
||||
if (this.state.previews.length > 20) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="post-create">
|
||||
<div id={this.props.rootId} className="post-create-body comment-create-body">
|
||||
<Textbox
|
||||
onUserInput={this.handleUserInput}
|
||||
onKeyPress={this.commentMsgKeyPress}
|
||||
messageText={this.state.messageText}
|
||||
createMessage="Create a comment..."
|
||||
initialText=""
|
||||
id="reply_textbox"
|
||||
ref="textbox" />
|
||||
<FileUpload
|
||||
setUploads={this.setUploads}
|
||||
onFileUpload={this.handleFileUpload}
|
||||
onUploadError={this.handleUploadError} />
|
||||
</div>
|
||||
<MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} />
|
||||
<div className={post_error ? 'has-error' : 'post-create-footer'}>
|
||||
<input type="button" className="btn btn-primary comment-btn pull-right" value="Add Comment" onClick={this.handleSubmit} />
|
||||
{ post_error }
|
||||
{ server_error }
|
||||
{ limit_previews }
|
||||
</div>
|
||||
</div>
|
||||
{ preview }
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
273
web/react/components/create_post.jsx
Обычный файл
273
web/react/components/create_post.jsx
Обычный файл
@@ -0,0 +1,273 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var MsgTyping = require('./msg_typing.jsx');
|
||||
var Textbox = require('./textbox.jsx');
|
||||
var FileUpload = require('./file_upload.jsx');
|
||||
var FilePreview = require('./file_preview.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
lastTime: 0,
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.uploadsInProgress > 0) return;
|
||||
|
||||
if (this.state.submitting) return;
|
||||
|
||||
var post = {};
|
||||
post.filenames = [];
|
||||
|
||||
post.message = this.state.messageText;
|
||||
|
||||
var repRegex = new RegExp("<br>", "g");
|
||||
if (post.message.replace(repRegex, " ").trim().length === 0
|
||||
&& this.state.previews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.message.length > Constants.CHARACTER_LIMIT) {
|
||||
this.setState({ post_error: 'Post length must be less than '+Constants.CHARACTER_LIMIT+' characters.' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ submitting: true });
|
||||
|
||||
var user_id = UserStore.getCurrentId();
|
||||
|
||||
if (post.message.indexOf("/") == 0) {
|
||||
client.executeCommand(
|
||||
this.state.channel_id,
|
||||
post.message,
|
||||
false,
|
||||
function(data) {
|
||||
PostStore.storeDraft(data.channel_id, user_id, null);
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
|
||||
|
||||
if (data.goto_location.length > 0) {
|
||||
window.location.href = data.goto_location;
|
||||
}
|
||||
}.bind(this),
|
||||
function(err){
|
||||
var state = {}
|
||||
state.server_error = err.message;
|
||||
state.submitting = false;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
post.channel_id = this.state.channel_id;
|
||||
post.filenames = this.state.previews;
|
||||
|
||||
client.createPost(post, ChannelStore.getCurrent(),
|
||||
function(data) {
|
||||
PostStore.storeDraft(data.channel_id, data.user_id, null);
|
||||
this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
|
||||
this.resizePostHolder();
|
||||
AsyncClient.getPosts(true);
|
||||
|
||||
var channel = ChannelStore.get(this.state.channel_id);
|
||||
var member = ChannelStore.getMember(this.state.channel_id);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = (new Date).getTime();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
var state = {}
|
||||
state.server_error = err.message;
|
||||
state.submitting = false;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
$(".post-list-holder-by-time").perfectScrollbar('update');
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.resizePostHolder();
|
||||
},
|
||||
postMsgKeyPress: function(e) {
|
||||
if (e.which == 13 && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
this.refs.textbox.getDOMNode().blur();
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
|
||||
var t = new Date().getTime();
|
||||
if ((t - this.lastTime) > 5000) {
|
||||
SocketStore.sendMessage({channel_id: this.state.channel_id, action: "typing", props: {"parent_id": ""}, state: {} });
|
||||
this.lastTime = t;
|
||||
}
|
||||
},
|
||||
handleUserInput: function(messageText) {
|
||||
this.resizePostHolder();
|
||||
this.setState({messageText: messageText});
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
if (!draft) {
|
||||
draft = {}
|
||||
draft['previews'] = [];
|
||||
draft['uploadsInProgress'] = 0;
|
||||
}
|
||||
draft['message'] = messageText;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
},
|
||||
resizePostHolder: function() {
|
||||
var height = $(window).height() - $(this.refs.topDiv.getDOMNode()).height() - $('#error_bar').outerHeight() - 50;
|
||||
$(".post-list-holder-by-time").css("height", height + "px");
|
||||
$(window).trigger('resize');
|
||||
},
|
||||
handleFileUpload: function(newPreviews, channel_id) {
|
||||
var draft = PostStore.getDraft(channel_id, UserStore.getCurrentId());
|
||||
if (!draft) {
|
||||
draft = {}
|
||||
draft['message'] = '';
|
||||
draft['uploadsInProgress'] = 0;
|
||||
draft['previews'] = [];
|
||||
}
|
||||
|
||||
if (channel_id === this.state.channel_id) {
|
||||
var num = this.state.uploadsInProgress;
|
||||
var oldPreviews = this.state.previews;
|
||||
var previews = oldPreviews.concat(newPreviews);
|
||||
|
||||
draft['previews'] = previews;
|
||||
draft['uploadsInProgress'] = num-1;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
|
||||
this.setState({previews: previews, uploadsInProgress:num-1});
|
||||
} else {
|
||||
draft['previews'] = draft['previews'].concat(newPreviews);
|
||||
draft['uploadsInProgress'] = draft['uploadsInProgress'] > 0 ? draft['uploadsInProgress'] - 1 : 0;
|
||||
PostStore.storeDraft(channel_id, UserStore.getCurrentId(), draft);
|
||||
}
|
||||
},
|
||||
handleUploadError: function(err) {
|
||||
this.setState({ server_error: err });
|
||||
},
|
||||
removePreview: function(filename) {
|
||||
var previews = this.state.previews;
|
||||
for (var i = 0; i < previews.length; i++) {
|
||||
if (previews[i] === filename) {
|
||||
previews.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
if (!draft) {
|
||||
draft = {}
|
||||
draft['message'] = '';
|
||||
draft['uploadsInProgress'] = 0;
|
||||
}
|
||||
draft['previews'] = previews;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
this.setState({previews: previews});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
this.resizePostHolder();
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var channel_id = ChannelStore.getCurrentId();
|
||||
if (this.state.channel_id != channel_id) {
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
var previews = [];
|
||||
var messageText = '';
|
||||
var uploadsInProgress = 0;
|
||||
if (draft) {
|
||||
previews = draft['previews'];
|
||||
messageText = draft['message'];
|
||||
uploadsInProgress = draft['uploadsInProgress'];
|
||||
}
|
||||
this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, post_error: null, previews: previews, uploadsInProgress: uploadsInProgress });
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
PostStore.clearDraftUploads();
|
||||
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
var previews = [];
|
||||
var messageText = '';
|
||||
if (draft) {
|
||||
previews = draft['previews'];
|
||||
messageText = draft['message'];
|
||||
}
|
||||
return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
|
||||
},
|
||||
setUploads: function(val) {
|
||||
var num = this.state.uploadsInProgress + val;
|
||||
var draft = PostStore.getCurrentDraft();
|
||||
if (!draft) {
|
||||
draft = {}
|
||||
draft['message'] = '';
|
||||
draft['previews'] = [];
|
||||
}
|
||||
draft['uploadsInProgress'] = num;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
this.setState({uploadsInProgress: num});
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
|
||||
|
||||
var preview = <div/>;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress} />
|
||||
);
|
||||
}
|
||||
var limit_previews = ""
|
||||
if (this.state.previews.length > 5) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
|
||||
}
|
||||
if (this.state.previews.length > 20) {
|
||||
limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="create_post" ref="topDiv" role="form" onSubmit={this.handleSubmit}>
|
||||
<div className="post-create">
|
||||
<div className="post-create-body">
|
||||
<Textbox
|
||||
onUserInput={this.handleUserInput}
|
||||
onKeyPress={this.postMsgKeyPress}
|
||||
messageText={this.state.messageText}
|
||||
createMessage="Create a post..."
|
||||
channelId={this.state.channel_id}
|
||||
id="post_textbox"
|
||||
ref="textbox" />
|
||||
<FileUpload
|
||||
setUploads={this.setUploads}
|
||||
onFileUpload={this.handleFileUpload}
|
||||
onUploadError={this.handleUploadError} />
|
||||
</div>
|
||||
<div className={post_error ? 'post-create-footer has-error' : 'post-create-footer'}>
|
||||
{ post_error }
|
||||
{ server_error }
|
||||
{ limit_previews }
|
||||
{ preview }
|
||||
<MsgTyping channelId={this.state.channel_id} parentId=""/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
58
web/react/components/delete_channel_modal.jsx
Обычный файл
58
web/react/components/delete_channel_modal.jsx
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client =require('../utils/client.jsx');
|
||||
var AsyncClient =require('../utils/async_client.jsx');
|
||||
var ChannelStore =require('../stores/channel_store.jsx')
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleDelete: function(e) {
|
||||
if (this.state.channel_id.length != 26) return;
|
||||
|
||||
Client.deleteChannel(this.state.channel_id,
|
||||
function(data) {
|
||||
AsyncClient.getChannels(true);
|
||||
window.location.href = '/channels/town-square';
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "handleDelete");
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = $(e.relatedTarget);
|
||||
self.setState({ title: button.attr('data-title'), channel_id: button.attr('data-channelid') });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { title: "", channel_id: "" };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var channelType = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'P' ? "private group" : "channel"
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="delete_channel" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title">Confirm DELETE Channel</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>
|
||||
Are you sure you wish to delete the {this.state.title} {channelType}?
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleDelete}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
108
web/react/components/delete_post_modal.jsx
Обычный файл
108
web/react/components/delete_post_modal.jsx
Обычный файл
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleDelete: function(e) {
|
||||
Client.deletePost(this.state.channel_id, this.state.post_id,
|
||||
function(data) {
|
||||
var selected_list = this.state.selectedList;
|
||||
if (selected_list && selected_list.order && selected_list.order.length > 0) {
|
||||
var selected_post = selected_list.posts[selected_list.order[0]];
|
||||
if ((selected_post.id === this.state.post_id && this.state.title === "Post") || selected_post.root_id === this.state.post_id) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
} else if (selected_post.id === this.state.post_id && this.state.title === "Comment") {
|
||||
if (selected_post.root_id && selected_post.root_id.length > 0 && selected_list.posts[selected_post.root_id]) {
|
||||
selected_list.order = [selected_post.root_id];
|
||||
delete selected_list.posts[selected_post.id];
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: selected_list
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
AsyncClient.getPosts(true, this.state.channel_id);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "deletePost");
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var newState = {};
|
||||
if(sessionStorage.getItem('edit_state_transfer')) {
|
||||
newState = JSON.parse(sessionStorage.getItem('edit_state_transfer'));
|
||||
sessionStorage.removeItem('edit_state_transfer');
|
||||
} else {
|
||||
var button = e.relatedTarget;
|
||||
newState = { title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments') };
|
||||
}
|
||||
self.setState(newState)
|
||||
});
|
||||
PostStore.addSelectedPostChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeSelectedPostChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newList = PostStore.getSelectedPost();
|
||||
if (!utils.areStatesEqual(this.state.selectedList, newList)) {
|
||||
this.setState({ selectedList: newList });
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { title: "", post_id: "", channel_id: "", selectedList: PostStore.getSelectedPost(), comments: 0 };
|
||||
},
|
||||
render: function() {
|
||||
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" id="delete_post" ref="modal" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog modal-push-down">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title">Confirm {this.state.title} Delete</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
Are you sure you want to delete the {this.state.title.toLowerCase()}?
|
||||
<br/>
|
||||
<br/>
|
||||
{ this.state.comments > 0 ?
|
||||
"This post has " + this.state.comments + " comment(s) on it."
|
||||
: "" }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleDelete}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
57
web/react/components/edit_channel_modal.jsx
Обычный файл
57
web/react/components/edit_channel_modal.jsx
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleEdit: function(e) {
|
||||
var data = {}
|
||||
data["channel_id"] = this.state.channel_id;
|
||||
if (data["channel_id"].length !== 26) return;
|
||||
data["channel_description"] = this.state.description.trim();
|
||||
|
||||
Client.updateChannelDesc(data,
|
||||
function(data) {
|
||||
AsyncClient.getChannels(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "updateChannelDesc");
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleUserInput: function(e) {
|
||||
this.setState({ description: e.target.value });
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid') });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { description: "", title: "", channel_id: "" };
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="edit_channel" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" ref="title">Edit {this.state.title} Description</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<textarea className="form-control" rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" className="btn btn-primary" data-dismiss="modal" onClick={this.handleEdit}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
100
web/react/components/edit_post_modal.jsx
Обычный файл
100
web/react/components/edit_post_modal.jsx
Обычный файл
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Textbox = require('./textbox.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleEdit: function(e) {
|
||||
var updatedPost = {};
|
||||
updatedPost.message = this.state.editText.trim();
|
||||
|
||||
if (updatedPost.message.length === 0) {
|
||||
var tempState = this.state;
|
||||
delete tempState.editText;
|
||||
sessionStorage.setItem('edit_state_transfer', JSON.stringify(tempState));
|
||||
$("#edit_post").modal('hide');
|
||||
$("#delete_post").modal('show');
|
||||
return;
|
||||
}
|
||||
|
||||
updatedPost.id = this.state.post_id
|
||||
updatedPost.channel_id = this.state.channel_id
|
||||
|
||||
Client.updatePost(updatedPost,
|
||||
function(data) {
|
||||
AsyncClient.getPosts(true, this.state.channel_id);
|
||||
window.scrollTo(0, 0);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "updatePost");
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
$("#edit_post").modal('hide');
|
||||
},
|
||||
handleEditInput: function(editText) {
|
||||
this.setState({ editText: editText });
|
||||
},
|
||||
handleEditKeyPress: function(e) {
|
||||
if (e.which == 13 && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
this.refs.editbox.getDOMNode().blur();
|
||||
this.handleEdit(e);
|
||||
}
|
||||
},
|
||||
handleUserInput: function(e) {
|
||||
this.setState({ editText: e.target.value });
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) {
|
||||
self.setState({ editText: "", title: "", channel_id: "", post_id: "", comments: 0 });
|
||||
});
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments') });
|
||||
});
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('shown.bs.modal', function(e) {
|
||||
self.refs.editbox.resize();
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { editText: "", title: "", post_id: "", channel_id: "", comments: 0 };
|
||||
},
|
||||
render: function() {
|
||||
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade edit-modal" ref="modal" id="edit_post" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog modal-push-down">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={this.handleEditClose}><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title">Edit {this.state.title}</h4>
|
||||
</div>
|
||||
<div className="edit-modal-body modal-body">
|
||||
<Textbox
|
||||
onUserInput={this.handleEditInput}
|
||||
onKeyPress={this.handleEditKeyPress}
|
||||
messageText={this.state.editText}
|
||||
createMessage="Edit the post..."
|
||||
id="edit_textbox"
|
||||
ref="editbox"
|
||||
/>
|
||||
{ error }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" className="btn btn-primary" onClick={this.handleEdit}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
35
web/react/components/email_verify.jsx
Обычный файл
35
web/react/components/email_verify.jsx
Обычный файл
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleResend: function() {
|
||||
window.location.href = window.location.href + "&resend=true"
|
||||
},
|
||||
render: function() {
|
||||
var title = "";
|
||||
var body = "";
|
||||
var resend = "";
|
||||
if (this.props.isVerified === "true") {
|
||||
title = config.SiteName + " Email Verified";
|
||||
body = <p>Your email has been verified! Click <a href="/">here</a> to log in.</p>;
|
||||
} else {
|
||||
title = config.SiteName + " Email Not Verified";
|
||||
body = <p>Please verify your email address. Check your inbox for an email.</p>;
|
||||
resend = <button onClick={this.handleResend} className="btn btn-primary">Resend Email</button>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-sm-offset-4 col-sm-4">
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<h3 className="panel-title">{ title }</h3>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{ body }
|
||||
{ resend }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
69
web/react/components/error_bar.jsx
Обычный файл
69
web/react/components/error_bar.jsx
Обычный файл
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ErrorStore = require('../stores/error_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getStateFromStores() {
|
||||
var error = ErrorStore.getLastError();
|
||||
if (error) {
|
||||
return { message: error.message };
|
||||
} else {
|
||||
return { message: null };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ErrorStore.addChangeListener(this._onChange);
|
||||
$('body').css('padding-top', $('#error_bar').outerHeight());
|
||||
$(window).resize(function(){
|
||||
$('body').css('padding-top', $('#error_bar').outerHeight());
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ErrorStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
if (newState.message) {
|
||||
var self = this;
|
||||
setTimeout(function(){self.handleClose();}, 10000);
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
handleClose: function(e) {
|
||||
if (e) e.preventDefault();
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_ERROR,
|
||||
err: null
|
||||
});
|
||||
$('body').css('padding-top', '0');
|
||||
},
|
||||
getInitialState: function() {
|
||||
var state = getStateFromStores();
|
||||
if (state.message) {
|
||||
var self = this;
|
||||
setTimeout(function(){self.handleClose();}, 10000);
|
||||
}
|
||||
return state;
|
||||
},
|
||||
render: function() {
|
||||
var message = this.state.message;
|
||||
if (message) {
|
||||
return (
|
||||
<div className="error-bar">
|
||||
<span className="error-text">{message}</span>
|
||||
<a href="#" className="error-close pull-right" onClick={this.handleClose}>×</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
});
|
||||
54
web/react/components/file_preview.jsx
Обычный файл
54
web/react/components/file_preview.jsx
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleRemove: function(e) {
|
||||
var previewDiv = e.target.parentNode.parentNode;
|
||||
this.props.onRemove(previewDiv.dataset.filename);
|
||||
},
|
||||
render: function() {
|
||||
var previews = [];
|
||||
this.props.files.forEach(function(filename) {
|
||||
|
||||
var filenameSplit = filename.split('.');
|
||||
var ext = filenameSplit[filenameSplit.length-1];
|
||||
var type = utils.getFileType(ext);
|
||||
|
||||
if (type === "image") {
|
||||
previews.push(
|
||||
<div key={filename} className="preview-div" data-filename={filename}>
|
||||
<img className="preview-img" src={filename}/>
|
||||
<a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
previews.push(
|
||||
<div key={filename} className="preview-div custom-file" data-filename={filename}>
|
||||
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
||||
<a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
for (var i = 0; i < this.props.uploadsInProgress; i++) {
|
||||
previews.push(
|
||||
<div className="preview-div">
|
||||
<img className="spinner" src="/static/images/load.gif"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="preview-container">
|
||||
{previews}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
129
web/react/components/file_upload.jsx
Обычный файл
129
web/react/components/file_upload.jsx
Обычный файл
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleChange: function() {
|
||||
var element = $(this.refs.fileInput.getDOMNode());
|
||||
var files = element.prop('files');
|
||||
|
||||
this.props.onUploadError(null);
|
||||
|
||||
//This looks redundant, but must be done this way due to
|
||||
//setState being an asynchronous call
|
||||
var numFiles = 0;
|
||||
for(var i = 0; i < files.length && i <= 20 ; i++) {
|
||||
if (files[i].size <= Constants.MAX_FILE_SIZE) {
|
||||
numFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.setUploads(numFiles);
|
||||
|
||||
for (var i = 0; i < files.length && i <= 20; i++) {
|
||||
if (files[i].size > Constants.MAX_FILE_SIZE) {
|
||||
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
|
||||
continue;
|
||||
}
|
||||
|
||||
var channel_id = ChannelStore.getCurrentId();
|
||||
|
||||
// Prepare data to be uploaded.
|
||||
formData = new FormData();
|
||||
formData.append('channel_id', channel_id);
|
||||
formData.append('files', files[i], files[i].name);
|
||||
|
||||
client.uploadFile(formData,
|
||||
function(data) {
|
||||
parsedData = $.parseJSON(data);
|
||||
this.props.onFileUpload(parsedData['filenames'], channel_id);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.props.setUploads(-1);
|
||||
this.props.onUploadError(err);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
// clear file input for all modern browsers
|
||||
try{
|
||||
element[0].value = '';
|
||||
if(element.value){
|
||||
element[0].type = "text";
|
||||
element[0].type = "file";
|
||||
}
|
||||
}catch(e){}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var inputDiv = this.refs.input.getDOMNode();
|
||||
var self = this;
|
||||
|
||||
document.addEventListener("paste", function(e) {
|
||||
var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0];
|
||||
|
||||
if (textarea != e.target && !$.contains(textarea,e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.props.onUploadError(null);
|
||||
|
||||
//This looks redundant, but must be done this way due to
|
||||
//setState being an asynchronous call
|
||||
var items = e.clipboardData.items;
|
||||
var numItems = 0;
|
||||
if (items) {
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
|
||||
ext = items[i].type.split("/")[1].toLowerCase();
|
||||
ext = ext == 'jpeg' ? 'jpg' : ext;
|
||||
|
||||
if (Constants.IMAGE_TYPES.indexOf(ext) < 0) return;
|
||||
|
||||
numItems++
|
||||
}
|
||||
}
|
||||
|
||||
self.props.setUploads(numItems);
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
var file = items[i].getAsFile();
|
||||
|
||||
ext = items[i].type.split("/")[1].toLowerCase();
|
||||
ext = ext == 'jpeg' ? 'jpg' : ext;
|
||||
|
||||
if (Constants.IMAGE_TYPES.indexOf(ext) < 0) return;
|
||||
|
||||
var channel_id = ChannelStore.getCurrentId();
|
||||
|
||||
formData = new FormData();
|
||||
formData.append('channel_id', channel_id);
|
||||
var d = new Date();
|
||||
var hour = d.getHours() < 10 ? "0" + d.getHours() : String(d.getHours());
|
||||
var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes());
|
||||
formData.append('files', file, "Image Pasted at "+d.getFullYear()+"-"+d.getMonth()+"-"+d.getDate()+" "+hour+"-"+min+"." + ext);
|
||||
|
||||
client.uploadFile(formData,
|
||||
function(data) {
|
||||
parsedData = $.parseJSON(data);
|
||||
self.props.onFileUpload(parsedData['filenames'], channel_id);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
self.props.onUploadError(err);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<span ref="input" className="btn btn-file"><span><i className="glyphicon glyphicon-paperclip"></i></span><input ref="fileInput" type="file" onChange={this.handleChange} multiple/></span>
|
||||
);
|
||||
}
|
||||
});
|
||||
72
web/react/components/find_team.jsx
Обычный файл
72
web/react/components/find_team.jsx
Обычный файл
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var state = { };
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim().toLowerCase();
|
||||
if (!email || !utils.isEmail(email)) {
|
||||
state.email_error = "Please enter a valid email address";
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
state.email_error = "";
|
||||
}
|
||||
|
||||
client.findTeamsSendEmail(email,
|
||||
function(data) {
|
||||
state.sent = true;
|
||||
this.setState(state);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
state.email_error = err.message;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null;
|
||||
|
||||
var divStyle = {
|
||||
"marginTop": "50px",
|
||||
}
|
||||
|
||||
if (this.state.sent) {
|
||||
return (
|
||||
<div>
|
||||
<h4>{"Find Your " + utils.toTitleCase(strings.Team)}</h4>
|
||||
<p>{"An email was sent with links to any " + strings.TeamPlural}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Find Your Team</h4>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<p>{"An email will be sent to this address with links to any " + strings.TeamPlural}</p>
|
||||
<div className="form-group">
|
||||
<label className='control-label'>Email</label>
|
||||
<div className={ email_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="text" ref="email" className="form-control" placeholder="you@domain.com" maxLength="128" />
|
||||
{ email_error }
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-primary" type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
55
web/react/components/get_link_modal.jsx
Обычный файл
55
web/react/components/get_link_modal.jsx
Обычный файл
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ZeroClipboardMixin = require('react-zeroclipboard-mixin');
|
||||
|
||||
ZeroClipboardMixin.ZeroClipboard.config({
|
||||
swfPath: '../../static/flash/ZeroClipboard.swf'
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
zeroclipboardElementsSelector: '[data-copy-btn]',
|
||||
mixins: [ ZeroClipboardMixin ],
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
if(this.refs.modal) {
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value') });
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var currentUser = UserStore.getCurrentUser()
|
||||
|
||||
if (currentUser != null) {
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="get_link" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" id="myModalLabel">{this.state.title} Link</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{"The link below is used for open " + strings.TeamPlural + " or if you allowed your " + strings.Team + " members to sign up using their " + strings.Company + " email addresses."}
|
||||
</p>
|
||||
<textarea className="form-control" readOnly="true" value={this.state.value}></textarea>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button data-copy-btn type="button" className="btn btn-primary" data-clipboard-text={this.state.value}>Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
});
|
||||
179
web/react/components/invite_member_modal.jsx
Обычный файл
179
web/react/components/invite_member_modal.jsx
Обычный файл
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Client =require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
var invite_ids = this.state.invite_ids;
|
||||
var count = invite_ids.length;
|
||||
var invites = [];
|
||||
var email_errors = this.state.email_errors;
|
||||
var first_name_errors = this.state.first_name_errors;
|
||||
var last_name_errors = this.state.last_name_errors;
|
||||
var valid = true;
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
var index = invite_ids[i];
|
||||
var invite = {};
|
||||
invite.email = this.refs["email"+index].getDOMNode().value.trim();
|
||||
if (!invite.email || !utils.isEmail(invite.email)) {
|
||||
email_errors[index] = "Please enter a valid email address";
|
||||
valid = false;
|
||||
} else {
|
||||
email_errors[index] = "";
|
||||
}
|
||||
|
||||
if (config.AllowInviteNames) {
|
||||
invite.first_name = this.refs["first_name"+index].getDOMNode().value.trim();
|
||||
if (!invite.first_name ) {
|
||||
first_name_errors[index] = "This is a required field";
|
||||
valid = false;
|
||||
} else {
|
||||
first_name_errors[index] = "";
|
||||
}
|
||||
|
||||
invite.last_name = this.refs["last_name"+index].getDOMNode().value.trim();
|
||||
if (!invite.last_name ) {
|
||||
last_name_errors[index] = "This is a required field";
|
||||
valid = false;
|
||||
} else {
|
||||
last_name_errors[index] = "";
|
||||
}
|
||||
}
|
||||
|
||||
invites.push(invite);
|
||||
}
|
||||
|
||||
this.setState({ email_errors: email_errors, first_name_errors: first_name_errors, last_name_errors: last_name_errors });
|
||||
|
||||
if (!valid || invites.length === 0) return;
|
||||
|
||||
var data = {}
|
||||
data["invites"] = invites;
|
||||
|
||||
Client.inviteMembers(data,
|
||||
function() {
|
||||
$(this.refs.modal.getDOMNode()).modal('hide');
|
||||
for (var i = 0; i < invite_ids.length; i++) {
|
||||
var index = invite_ids[i];
|
||||
this.refs["email"+index].getDOMNode().value = "";
|
||||
if (config.AllowInviteNames) {
|
||||
this.refs["first_name"+index].getDOMNode().value = "";
|
||||
this.refs["last_name"+index].getDOMNode().value = "";
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
invite_ids: [0],
|
||||
id_count: 0,
|
||||
email_errors: {},
|
||||
first_name_errors: {},
|
||||
last_name_errors: {}
|
||||
});
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err });
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
$(this.refs.modalBody.getDOMNode()).css('max-height', $(window).height() - 200);
|
||||
$(this.refs.modalBody.getDOMNode()).css('overflow-y', 'scroll');
|
||||
},
|
||||
addInviteFields: function() {
|
||||
var count = this.state.id_count + 1;
|
||||
var invite_ids = this.state.invite_ids;
|
||||
invite_ids.push(count);
|
||||
this.setState({ invite_ids: invite_ids, id_count: count });
|
||||
},
|
||||
removeInviteFields: function(index) {
|
||||
var invite_ids = this.state.invite_ids;
|
||||
var i = invite_ids.indexOf(index);
|
||||
if (index > -1) invite_ids.splice(i, 1);
|
||||
this.setState({ invite_ids: invite_ids });
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
invite_ids: [0],
|
||||
id_count: 0,
|
||||
email_errors: {},
|
||||
first_name_errors: {},
|
||||
last_name_errors: {}
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
var currentUser = UserStore.getCurrentUser()
|
||||
|
||||
if (currentUser != null) {
|
||||
var invite_sections = [];
|
||||
var invite_ids = this.state.invite_ids;
|
||||
var self = this;
|
||||
for (var i = 0; i < invite_ids.length; i++) {
|
||||
var index = invite_ids[i];
|
||||
var email_error = this.state.email_errors[index] ? <label className='control-label'>{ this.state.email_errors[index] }</label> : null;
|
||||
var first_name_error = this.state.first_name_errors[index] ? <label className='control-label'>{ this.state.first_name_errors[index] }</label> : null;
|
||||
var last_name_error = this.state.last_name_errors[index] ? <label className='control-label'>{ this.state.last_name_errors[index] }</label> : null;
|
||||
|
||||
invite_sections[index] = (
|
||||
<div key={"key" + index}>
|
||||
{ i ?
|
||||
<div>
|
||||
<button type="button" className="btn remove__member" onClick={function(){self.removeInviteFields(index);}}>×</button>
|
||||
</div>
|
||||
: ""}
|
||||
<div className={ email_error ? "form-group invite has-error" : "form-group invite" }>
|
||||
<input onKeyUp={this.displayNameKeyUp} type="text" ref={"email"+index} className="form-control" placeholder="email@domain.com" maxLength="64" />
|
||||
{ email_error }
|
||||
</div>
|
||||
{ config.AllowInviteNames ?
|
||||
<div className={ first_name_error ? "form-group invite has-error" : "form-group invite" }>
|
||||
<input type="text" className="form-control" ref={"first_name"+index} placeholder="First name" maxLength="64" />
|
||||
{ first_name_error }
|
||||
</div>
|
||||
: "" }
|
||||
{ config.AllowInviteNames ?
|
||||
<div className={ last_name_error ? "form-group invite has-error" : "form-group invite" }>
|
||||
<input type="text" className="form-control" ref={"last_name"+index} placeholder="Last name" maxLength="64" />
|
||||
{ last_name_error }
|
||||
</div>
|
||||
: "" }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="invite_member" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close" data-reactid=".5.0.0.0.0"><span aria-hidden="true" data-reactid=".5.0.0.0.0.0">×</span></button>
|
||||
<h4 className="modal-title" id="myModalLabel">Invite New Member</h4>
|
||||
</div>
|
||||
<div ref="modalBody" className="modal-body">
|
||||
<form role="form">
|
||||
{ invite_sections }
|
||||
</form>
|
||||
{ server_error }
|
||||
<button type="button" className="btn btn-default" onClick={this.addInviteFields}>Add another</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='control-label'>People invited automatically join Town Square channel.</label>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button onClick={this.handleSubmit} type="button" className="btn btn-primary">Send Invitations</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
});
|
||||
197
web/react/components/login.jsx
Обычный файл
197
web/react/components/login.jsx
Обычный файл
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
|
||||
var FindTeamDomain = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var state = { }
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.server_error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.server_error = "";
|
||||
this.setState(state);
|
||||
|
||||
client.findTeamByDomain(domain,
|
||||
function(data) {
|
||||
console.log(data);
|
||||
if (data) {
|
||||
window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub();
|
||||
}
|
||||
else {
|
||||
this.state.server_error = "We couldn't find your " + strings.TeamPlural + ".";
|
||||
this.setState(this.state);
|
||||
}
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.server_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null;
|
||||
|
||||
return (
|
||||
<div className="signup-team__container">
|
||||
<div>
|
||||
<span className="signup-team__name">{ config.SiteName }</span>
|
||||
<br/>
|
||||
<span className="signup-team__subdomain">Enter your {strings.TeamPlural} domain.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
{ server_error }
|
||||
<input type="text" className="form-control" name="domain" ref="domain" placeholder="teamdomain" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button type="submit" className="btn btn-primary">Continue</button>
|
||||
</div>
|
||||
<div>
|
||||
<span>Don't remember your {strings.TeamPlural} domain? <a href="/find_team">Find it here</a></span>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
<span>{"Want to create your own " + strings.Team + "?"} <a href="/" className="signup-team-login">Sign up now</a></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var state = { }
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.server_error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim();
|
||||
if (!email) {
|
||||
state.server_error = "An email is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!password) {
|
||||
state.server_error = "A password is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.server_error = "";
|
||||
this.setState(state);
|
||||
|
||||
client.loginByEmail(domain, email, password,
|
||||
function(data) {
|
||||
UserStore.setLastDomain(domain);
|
||||
UserStore.setLastEmail(email);
|
||||
UserStore.setCurrentUser(data);
|
||||
|
||||
var redirect = utils.getUrlParameter("redirect");
|
||||
if (redirect) {
|
||||
window.location.href = decodeURI(redirect);
|
||||
} else {
|
||||
window.location.href = '/channels/town-square';
|
||||
}
|
||||
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
if (err.message == "Login failed because email address has not been verified") {
|
||||
window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email);
|
||||
return;
|
||||
}
|
||||
state.server_error = err.message;
|
||||
this.valid = false;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null;
|
||||
var priorEmail = UserStore.getLastEmail() !== "undefined" ? UserStore.getLastEmail() : ""
|
||||
|
||||
var emailParam = utils.getUrlParameter("email");
|
||||
if (emailParam) {
|
||||
priorEmail = decodeURIComponent(emailParam);
|
||||
}
|
||||
|
||||
var subDomainClass = "form-control hidden";
|
||||
var subDomain = utils.getSubDomain();
|
||||
|
||||
if (utils.isTestDomain()) {
|
||||
subDomainClass = "form-control";
|
||||
subDomain = UserStore.getLastDomain();
|
||||
} else if (subDomain == "") {
|
||||
return (<FindTeamDomain />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="signup-team__container">
|
||||
<div>
|
||||
<span className="signup-team__name">{ subDomain }</span>
|
||||
<br/>
|
||||
<span className="signup-team__subdomain">{ utils.getDomainWithOutSub() }</span>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
{ server_error }
|
||||
<input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="email" className="form-control" name="email" defaultValue={priorEmail} ref="email" placeholder="Email" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="password" className="form-control" name="password" ref="password" placeholder="Password" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button type="submit" className="btn btn-primary">Sign in</button>
|
||||
</div>
|
||||
<div className="form-group form-group--small">
|
||||
<span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<a href="/reset_password">I forgot my password</a>
|
||||
</div>
|
||||
<div className="external-link">
|
||||
<span>{"Want to create your own " + strings.Team + "?"} <a href={config.HomeLink} className="signup-team-login">Sign up now</a></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
34
web/react/components/member_list.jsx
Обычный файл
34
web/react/components/member_list.jsx
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var MemberListItem = require('./member_list_item.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
var members = [];
|
||||
|
||||
if (this.props.memberList != null) {
|
||||
members = this.props.memberList;
|
||||
}
|
||||
|
||||
var message = "";
|
||||
if (members.length === 0)
|
||||
message = <span>No users to add or manage.</span>;
|
||||
|
||||
return (
|
||||
<div className="member-list-holder">
|
||||
{members.map(function(member) {
|
||||
return <MemberListItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
isAdmin={this.props.isAdmin}
|
||||
handleInvite={this.props.handleInvite}
|
||||
handleRemove={this.props.handleRemove}
|
||||
handleMakeAdmin={this.props.handleMakeAdmin}
|
||||
/>;
|
||||
}, this)}
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
67
web/react/components/member_list_item.jsx
Обычный файл
67
web/react/components/member_list_item.jsx
Обычный файл
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleInvite: function() {
|
||||
this.props.handleInvite(this.props.member.id);
|
||||
},
|
||||
handleRemove: function() {
|
||||
this.props.handleRemove(this.props.member.id);
|
||||
},
|
||||
handleMakeAdmin: function() {
|
||||
this.props.handleMakeAdmin(this.props.member.id);
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var member = this.props.member;
|
||||
var isAdmin = this.props.isAdmin;
|
||||
var isMemberAdmin = member.roles.indexOf("admin") > -1;
|
||||
|
||||
if (member.roles === '') {
|
||||
member.roles = 'Member';
|
||||
} else {
|
||||
member.roles = member.roles.charAt(0).toUpperCase() + member.roles.slice(1);
|
||||
}
|
||||
|
||||
var invite;
|
||||
if (member.invited && this.props.handleInvite) {
|
||||
invite = <span className="member-role">Added</span>;
|
||||
} else if (this.props.handleInvite) {
|
||||
invite = <a onClick={this.handleInvite} className="btn btn-sm btn-primary member-invite"><i className="glyphicon glyphicon-envelope"/> Add</a>;
|
||||
} else if (isAdmin && !isMemberAdmin && (member.id != UserStore.getCurrentId())) {
|
||||
var self = this;
|
||||
invite = (
|
||||
<div className="dropdown member-drop">
|
||||
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true">
|
||||
<span>{member.roles} </span>
|
||||
<span className="caret"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu member-menu" role="menu" aria-labelledby="channel_header_dropdown">
|
||||
{ this.props.handleMakeAdmin ?
|
||||
<li role="presentation"><a role="menuitem" onClick={self.handleMakeAdmin}>Make Admin</a></li>
|
||||
: "" }
|
||||
{ this.props.handleRemove ?
|
||||
<li role="presentation"><a role="menuitem" onClick={self.handleRemove}>Remove Member</a></li>
|
||||
: "" }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
invite = <div className="member-drop"><span>{member.roles} </span><span className="caret invisible"></span></div>;
|
||||
}
|
||||
|
||||
var email = member.email.length > 0 ? member.email : "";
|
||||
|
||||
return (
|
||||
<div className="row member-div">
|
||||
<img className="post-profile-img pull-left" src={"/api/v1/users/" + member.id + "/image"} height="36" width="36" />
|
||||
<span className="member-name">{member.username}</span>
|
||||
<span className="member-email">{email}</span>
|
||||
{ invite }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
120
web/react/components/member_list_team.jsx
Обычный файл
120
web/react/components/member_list_team.jsx
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
|
||||
var MemberListTeamItem = React.createClass({
|
||||
handleMakeMember: function() {
|
||||
var data = {};
|
||||
data["user_id"] = this.props.user.id;
|
||||
data["new_roles"] = "";
|
||||
|
||||
Client.updateRoles(data,
|
||||
function(data) {
|
||||
AsyncClient.getProfiles();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleMakeActive: function() {
|
||||
Client.updateActive(this.props.user.id, true,
|
||||
function(data) {
|
||||
AsyncClient.getProfiles();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleMakeNotActive: function() {
|
||||
Client.updateActive(this.props.user.id, false,
|
||||
function(data) {
|
||||
AsyncClient.getProfiles();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleMakeAdmin: function() {
|
||||
var data = {};
|
||||
data["user_id"] = this.props.user.id;
|
||||
data["new_roles"] = "admin";
|
||||
|
||||
Client.updateRoles(data,
|
||||
function(data) {
|
||||
AsyncClient.getProfiles();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ server_error: err.message });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <div style={{ clear: "both" }} className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null;
|
||||
var user = this.props.user;
|
||||
var currentRoles = "Member"
|
||||
|
||||
if (user.roles.length > 0) {
|
||||
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
|
||||
}
|
||||
|
||||
var email = user.email.length > 0 ? user.email : "";
|
||||
var showMakeMember = user.roles == "admin";
|
||||
var showMakeAdmin = user.roles == "";
|
||||
var showMakeActive = false;
|
||||
var showMakeNotActive = true;
|
||||
|
||||
if (user.delete_at > 0) {
|
||||
currentRoles = "Inactive";
|
||||
showMakeMember = false;
|
||||
showMakeAdmin = false;
|
||||
showMakeActive = true;
|
||||
showMakeNotActive = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row member-div">
|
||||
<img className="post-profile-img pull-left" src={"/api/v1/users/" + user.id + "/image"} height="36" width="36" />
|
||||
<span className="member-name">{user.full_name.trim() ? user.full_name : user.username}</span>
|
||||
<span className="member-email">{user.full_name.trim() ? user.username : email}</span>
|
||||
<div className="dropdown member-drop">
|
||||
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true">
|
||||
<span>{currentRoles} </span>
|
||||
<span className="caret"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu member-menu" role="menu" aria-labelledby="channel_header_dropdown">
|
||||
{ showMakeAdmin ? <li role="presentation"><a role="menuitem" onClick={this.handleMakeAdmin}>Make Admin</a></li> : "" }
|
||||
{ showMakeMember ? <li role="presentation"><a role="menuitem" onClick={this.handleMakeMember}>Make Member</a></li> : "" }
|
||||
{ showMakeActive ? <li role="presentation"><a role="menuitem" onClick={this.handleMakeActive}>Make Active</a></li> : "" }
|
||||
{ showMakeNotActive ? <li role="presentation"><a role="menuitem" onClick={this.handleMakeNotActive}>Make Inactive</a></li> : "" }
|
||||
</ul>
|
||||
</div>
|
||||
{ server_error }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="member-list-holder">
|
||||
{
|
||||
this.props.users.map(function(user) {
|
||||
return <MemberListTeamItem key={user.id} user={user} />;
|
||||
}, this)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
16
web/react/components/mention.jsx
Обычный файл
16
web/react/components/mention.jsx
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleClick: function() {
|
||||
this.props.handleClick(this.props.username);
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mentions-name" onClick={this.handleClick}>
|
||||
<img className="pull-left mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/>
|
||||
<span>@{this.props.username}</span><span style={{'color':'grey', 'marginLeft':'10px'}}>{this.props.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
127
web/react/components/mention_list.jsx
Обычный файл
127
web/react/components/mention_list.jsx
Обычный файл
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Mention = require('./mention.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
PostStore.addMentionDataChangeListener(this._onChange);
|
||||
|
||||
var self = this;
|
||||
$('#'+this.props.id).on('keypress.mentionlist',
|
||||
function(e) {
|
||||
if (!self.isEmpty() && self.state.mentionText != '-1' && e.which === 13) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
self.addFirstMention();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeMentionDataChangeListener(this._onChange);
|
||||
$('#'+this.props.id).off('keypress.mentionlist');
|
||||
},
|
||||
_onChange: function(id, mentionText, excludeList) {
|
||||
if (id !== this.props.id) return;
|
||||
|
||||
var newState = this.state;
|
||||
if (mentionText != null) newState.mentionText = mentionText;
|
||||
if (excludeList != null) newState.excludeUsers = excludeList;
|
||||
this.setState(newState);
|
||||
},
|
||||
handleClick: function(name) {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.RECIEVED_ADD_MENTION,
|
||||
id: this.props.id,
|
||||
username: name
|
||||
});
|
||||
|
||||
this.setState({ mentionText: '-1' });
|
||||
},
|
||||
addFirstMention: function() {
|
||||
if (!this.refs.mention0) return;
|
||||
this.refs.mention0.handleClick();
|
||||
},
|
||||
isEmpty: function() {
|
||||
return (!this.refs.mention0);
|
||||
},
|
||||
alreadyMentioned: function(username) {
|
||||
var excludeUsers = this.state.excludeUsers;
|
||||
for (var i = 0; i < excludeUsers.length; i++) {
|
||||
if (excludeUsers[i] === username) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { excludeUsers: [], mentionText: "-1" };
|
||||
},
|
||||
render: function() {
|
||||
var mentionText = this.state.mentionText;
|
||||
if (mentionText === '-1') return (<div/>);
|
||||
|
||||
var profiles = UserStore.getActiveOnlyProfiles();
|
||||
var users = [];
|
||||
for (var id in profiles) {
|
||||
users.push(profiles[id]);
|
||||
}
|
||||
|
||||
users.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
var mentions = {};
|
||||
var index = 0;
|
||||
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
if (Object.keys(mentions).length >= 25) break;
|
||||
if (this.alreadyMentioned(users[i].username)) continue;
|
||||
|
||||
var firstName = "", lastName = "";
|
||||
if (users[i].full_name.length > 0) {
|
||||
var splitName = users[i].full_name.split(' ');
|
||||
firstName = splitName[0].toLowerCase();
|
||||
lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : "";
|
||||
}
|
||||
|
||||
if (firstName.lastIndexOf(mentionText,0) === 0
|
||||
|| lastName.lastIndexOf(mentionText,0) === 0 || users[i].username.lastIndexOf(mentionText,0) === 0) {
|
||||
mentions[i+1] = (
|
||||
<Mention
|
||||
ref={'mention' + index}
|
||||
username={users[i].username}
|
||||
name={users[i].full_name}
|
||||
id={users[i].id}
|
||||
handleClick={this.handleClick} />
|
||||
);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
var numMentions = Object.keys(mentions).length;
|
||||
|
||||
if (numMentions < 1) return (<div/>);
|
||||
|
||||
var height = (numMentions*37) + 2;
|
||||
var width = $('#'+this.props.id).parent().width();
|
||||
var bottom = $(window).height() - $('#'+this.props.id).offset().top;
|
||||
var left = $('#'+this.props.id).offset().left;
|
||||
var max_height = $('#'+this.props.id).offset().top - 10;
|
||||
|
||||
return (
|
||||
<div className="mentions--top" style={{height: height, width: width, bottom: bottom, left: left}}>
|
||||
<div ref="mentionlist" className="mentions-box" style={{maxHeight: max_height, height: height, width: width}}>
|
||||
{ mentions }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
17
web/react/components/message_wrapper.jsx
Обычный файл
17
web/react/components/message_wrapper.jsx
Обычный файл
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
if (this.props.message) {
|
||||
var inner = utils.textToJsx(this.props.message, this.props.options);
|
||||
return (
|
||||
<div>{inner}</div>
|
||||
);
|
||||
} else {
|
||||
return <div/>
|
||||
}
|
||||
}
|
||||
});
|
||||
109
web/react/components/more_channels.jsx
Обычный файл
109
web/react/components/more_channels.jsx
Обычный файл
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var asyncClient = require('../utils/async_client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
channels: ChannelStore.getMoreAll(),
|
||||
server_error: null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addMoreChangeListener(this._onChange);
|
||||
$(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) {
|
||||
asyncClient.getMoreChannels(true);
|
||||
});
|
||||
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ channel_type: $(button).attr('data-channeltype') });
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeMoreChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState.channels, this.state.channels)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
var initState = getStateFromStores();
|
||||
initState.channel_type = "";
|
||||
return initState;
|
||||
},
|
||||
handleJoin: function(e) {
|
||||
var self = this;
|
||||
client.joinChannel(e,
|
||||
function(data) {
|
||||
$(self.refs.modal.getDOMNode()).modal('hide');
|
||||
asyncClient.getChannels(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.server_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
handleNewChannel: function() {
|
||||
$(this.refs.modal.getDOMNode()).modal('hide');
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
var outter = this;
|
||||
|
||||
return (
|
||||
<div className="modal fade" id="more_channels" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">More Channels</h4>
|
||||
<button data-toggle="modal" data-target="#new_channel" data-channeltype={this.state.channel_type} type="button" className="btn btn-primary channel-create-btn" onClick={this.handleNewChannel}>Create New Channel</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{this.state.channels.length ?
|
||||
<table className="more-channel-table table">
|
||||
<tbody>
|
||||
{this.state.channels.map(function(channel) {
|
||||
return (
|
||||
<tr key={channel.id}>
|
||||
<td>
|
||||
<p className="more-channel-name">{channel.display_name}</p>
|
||||
<p className="more-channel-description">{channel.description}</p>
|
||||
</td>
|
||||
<td className="td--action"><button onClick={outter.handleJoin.bind(outter, channel.id)} className="pull-right btn btn-primary">Join</button></td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
: <div className="no-channel-message">
|
||||
<p className="primary-message">No more channels to join</p>
|
||||
<p className="secondary-message">Click 'Create New Channel' to make a new one</p>
|
||||
</div>}
|
||||
{ server_error }
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
68
web/react/components/more_direct_channels.jsx
Обычный файл
68
web/react/components/more_direct_channels.jsx
Обычный файл
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ channels: $(button).data('channels') });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { channels: [] };
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
var directMessageItems = this.state.channels.map(function(channel) {
|
||||
var badge = "";
|
||||
var titleClass = ""
|
||||
|
||||
if (!channel.fake) {
|
||||
var active = channel.id === ChannelStore.getCurrentId() ? "active" : "";
|
||||
|
||||
if (channel.unread) {
|
||||
badge = <span className="badge pull-right small">{channel.unread}</span>;
|
||||
badgesActive = true;
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
return (
|
||||
<li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel, channel.teammate_username); $(self.refs.modal.getDOMNode()).modal('hide')}}>{badge}{channel.display_name}</a></li>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href={"/channels/"+channel.name}>{badge}{channel.display_name}</a></li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="modal fade" id="more_direct_channels" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">More Direct Messages</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
{directMessageItems}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
49
web/react/components/msg_typing.jsx
Обычный файл
49
web/react/components/msg_typing.jsx
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
timer: null,
|
||||
lastTime: 0,
|
||||
componentDidMount: function() {
|
||||
SocketStore.addChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
SocketStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function(msg) {
|
||||
if (msg.action == "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) {
|
||||
var outer = this;
|
||||
outer.timer = setInterval(function() {
|
||||
if ((new Date().getTime() - outer.lastTime) > 8000) {
|
||||
outer.setState({ text: "" });
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { text: "" };
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<span className="msg-typing">{ this.state.text }</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
351
web/react/components/navbar.jsx
Обычный файл
351
web/react/components/navbar.jsx
Обычный файл
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Sidebar = require('./sidebar.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var UserProfile = require('./user_profile.jsx');
|
||||
var MessageWrapper = require('./message_wrapper.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getCountsStateFromStores() {
|
||||
|
||||
var count = 0;
|
||||
var channels = ChannelStore.getAll();
|
||||
var members = ChannelStore.getAllMembers();
|
||||
|
||||
channels.forEach(function(channel) {
|
||||
var channelMember = members[channel.id];
|
||||
if (channel.type === 'D') {
|
||||
count += channel.total_msg_count - channelMember.msg_count;
|
||||
} else {
|
||||
if (channelMember.mention_count > 0) {
|
||||
count += channelMember.mention_count;
|
||||
} else if (channel.total_msg_count - channelMember.msg_count > 0) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { count: count }
|
||||
}
|
||||
|
||||
var NotifyCounts = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getCountsStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getCountsStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
if (this.state.count == 0) {
|
||||
return (<span></span>);
|
||||
}
|
||||
else {
|
||||
return (<span className="badge badge-notify">{ this.state.count }</span>);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var NavbarLoginForm = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var state = { }
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.server_error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim();
|
||||
if (!email) {
|
||||
state.server_error = "An email is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!password) {
|
||||
state.server_error = "A password is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.server_error = "";
|
||||
this.setState(state);
|
||||
|
||||
client.loginByEmail(domain, email, password,
|
||||
function(data) {
|
||||
UserStore.setLastDomain(domain);
|
||||
UserStore.setLastEmail(email);
|
||||
UserStore.setCurrentUser(data);
|
||||
|
||||
var redirect = utils.getUrlParameter("redirect");
|
||||
if (redirect) {
|
||||
window.location.href = decodeURI(redirect);
|
||||
} else {
|
||||
window.location.href = '/channels/town-square';
|
||||
}
|
||||
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
if (err.message == "Login failed because email address has not been verified") {
|
||||
window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email);
|
||||
return;
|
||||
}
|
||||
state.server_error = err.message;
|
||||
this.valid = false;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null;
|
||||
|
||||
var subDomain = utils.getSubDomain();
|
||||
var subDomainClass = "form-control hidden";
|
||||
|
||||
if (subDomain == "") {
|
||||
subDomain = UserStore.getLastDomain();
|
||||
subDomainClass = "form-control";
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="navbar-form navbar-right" onSubmit={this.handleSubmit}>
|
||||
<a href="/find_team">Find your team</a>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
{ server_error }
|
||||
<input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="text" className="form-control" name="email" defaultValue={UserStore.getLastEmail()} ref="email" placeholder="Email" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="password" className="form-control" name="password" ref="password" placeholder="Password" />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-default">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
channel: ChannelStore.getCurrent(),
|
||||
member: ChannelStore.getCurrentMember(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this._onChange);
|
||||
var self = this;
|
||||
$('.inner__wrap').click(self.hideSidebars);
|
||||
|
||||
$('body').on('click.infopopover', function(e){
|
||||
if ($(e.target).attr('data-toggle') !== 'popover'
|
||||
&& $(e.target).parents('.popover.in').length === 0) {
|
||||
$('.info-popover').popover('hide');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
handleLeave: function(e) {
|
||||
client.leaveChannel(this.state.channel.id,
|
||||
function(data) {
|
||||
AsyncClient.getChannels(true);
|
||||
window.location.href = '/channels/town-square';
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
AsyncClient.dispatchError(err, "handleLeave");
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
hideSidebars: function(e) {
|
||||
var windowWidth = $(window).outerWidth();
|
||||
if(windowWidth <= 768) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
|
||||
if (e.target.className != 'navbar-toggle' && e.target.className != 'icon-bar') {
|
||||
$('.inner__wrap').removeClass('move--right').removeClass('move--left').removeClass('move--left-small');
|
||||
$('.sidebar--left').removeClass('move--right');
|
||||
$('.sidebar--right').removeClass('move--left');
|
||||
$('.sidebar--menu').removeClass('move--left');
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleLeftSidebar: function() {
|
||||
$('.inner__wrap').toggleClass('move--right');
|
||||
$('.sidebar--left').toggleClass('move--right');
|
||||
},
|
||||
toggleRightSidebar: function() {
|
||||
$('.inner__wrap').toggleClass('move--left-small');
|
||||
$('.sidebar--menu').toggleClass('move--left');
|
||||
},
|
||||
_onChange: function() {
|
||||
this.setState(getStateFromStores());
|
||||
$("#navbar .navbar-brand .description").popover({placement : 'bottom', trigger: 'click', html: true});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var currentId = UserStore.getCurrentId();
|
||||
var channelName = "";
|
||||
var popoverContent = "";
|
||||
var channelTitle = this.props.teamName;
|
||||
var isAdmin = false;
|
||||
var isDirect = false;
|
||||
var description = ""
|
||||
|
||||
if (this.state.channel) {
|
||||
var channel = this.state.channel;
|
||||
description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true});
|
||||
popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>);
|
||||
channelName = this.state.channel.name;
|
||||
isAdmin = this.state.member.roles.indexOf("admin") > -1;
|
||||
|
||||
if (channel.type === 'O') {
|
||||
channelTitle = this.state.channel.display_name;
|
||||
} else if (channel.type === 'P') {
|
||||
channelTitle = this.state.channel.display_name;
|
||||
} else if (channel.type === 'D') {
|
||||
isDirect = true;
|
||||
if (this.state.users.length > 1) {
|
||||
if (this.state.users[0].id === currentId) {
|
||||
channelTitle = <UserProfile userId={this.state.users[1].id} />;
|
||||
} else {
|
||||
channelTitle = <UserProfile userId={this.state.users[0].id} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this.state.channel.description.length == 0){
|
||||
popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>);
|
||||
}
|
||||
}
|
||||
|
||||
var loginForm = currentId == null ? <NavbarLoginForm /> : null;
|
||||
var navbar_collapse_button = currentId != null ? null :
|
||||
<button type="button" className="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-1">
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
<span className="icon-bar"></span>
|
||||
<span className="icon-bar"></span>
|
||||
<span className="icon-bar"></span>
|
||||
</button>;
|
||||
var sidebar_collapse_button = currentId == null ? null :
|
||||
<button type="button" className="navbar-toggle" data-toggle="collapse" data-target="#sidebar-nav" onClick={this.toggleLeftSidebar}>
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
<span className="icon-bar"></span>
|
||||
<span className="icon-bar"></span>
|
||||
<span className="icon-bar"></span>
|
||||
<NotifyCounts />
|
||||
</button>;
|
||||
var right_sidebar_collapse_button= currentId == null ? null :
|
||||
<button type="button" className="navbar-toggle menu-toggle pull-right" data-toggle="collapse" data-target="#sidebar-nav" onClick={this.toggleRightSidebar}>
|
||||
<span className="dropdown__icon"></span>
|
||||
</button>;
|
||||
|
||||
|
||||
return (
|
||||
<nav className="navbar navbar-default navbar-fixed-top" role="navigation">
|
||||
<div className="container-fluid theme">
|
||||
<div className="navbar-header">
|
||||
{ navbar_collapse_button }
|
||||
{ sidebar_collapse_button }
|
||||
{ right_sidebar_collapse_button }
|
||||
{ !isDirect && this.state.channel ?
|
||||
<div className="navbar-brand">
|
||||
<div className="dropdown">
|
||||
<div data-toggle="popover" data-content={popoverContent} className="description info-popover"></div>
|
||||
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true">
|
||||
<strong className="heading">{channelTitle} </strong>
|
||||
<span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown">
|
||||
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Invite Members</a></li>
|
||||
{ isAdmin ?
|
||||
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li>
|
||||
: ""
|
||||
}
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li>
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li>
|
||||
{ isAdmin && channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li>
|
||||
: ""
|
||||
}
|
||||
{ isAdmin && channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li>
|
||||
: ""
|
||||
}
|
||||
{ channelName != Constants.DEFAULT_CHANNEL ?
|
||||
<li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li>
|
||||
: ""
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
: "" }
|
||||
{ isDirect && this.state.channel ?
|
||||
<div className="navbar-brand">
|
||||
<strong>
|
||||
<a href="#"><strong className="heading">{channelTitle}</strong></a>
|
||||
</strong>
|
||||
</div>
|
||||
: "" }
|
||||
{ !this.state.channel ?
|
||||
<div className="navbar-brand">
|
||||
<strong>
|
||||
<a href="/"><strong className="heading">{ channelTitle }</strong></a>
|
||||
</strong>
|
||||
</div>
|
||||
: "" }
|
||||
</div>
|
||||
<div className="collapse navbar-collapse" id="navbar-collapse-1">
|
||||
{ loginForm }
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
139
web/react/components/new_channel.jsx
Обычный файл
139
web/react/components/new_channel.jsx
Обычный файл
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var asyncClient = require('../utils/async_client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var channel = {};
|
||||
var state = { server_error: "" };
|
||||
|
||||
channel.display_name = this.refs.display_name.getDOMNode().value.trim();
|
||||
if (!channel.display_name) {
|
||||
state.display_name_error = "This field is required";
|
||||
state.inValid = true;
|
||||
}
|
||||
else if (channel.display_name.length > 22) {
|
||||
state.display_name_error = "This field must be less than 22 characters";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.display_name_error = "";
|
||||
}
|
||||
|
||||
channel.name = this.refs.channel_name.getDOMNode().value.trim();
|
||||
if (!channel.name) {
|
||||
state.name_error = "This field is required";
|
||||
state.inValid = true;
|
||||
}
|
||||
else if(channel.name.length > 22){
|
||||
state.name_error = "This field must be less than 22 characters";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
var cleaned_name = utils.cleanUpUrlable(channel.name);
|
||||
if (cleaned_name != channel.name) {
|
||||
state.name_error = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.name_error = "";
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (state.inValid)
|
||||
return;
|
||||
|
||||
var cu = UserStore.getCurrentUser();
|
||||
channel.team_id = cu.team_id;
|
||||
|
||||
channel.description = this.refs.channel_desc.getDOMNode().value.trim();
|
||||
channel.type = this.state.channel_type;
|
||||
|
||||
var self = this;
|
||||
client.createChannel(channel,
|
||||
function(data) {
|
||||
this.refs.display_name.getDOMNode().value = "";
|
||||
this.refs.channel_name.getDOMNode().value = "";
|
||||
this.refs.channel_desc.getDOMNode().value = "";
|
||||
|
||||
$(self.refs.modal.getDOMNode()).modal('hide');
|
||||
window.location.href = "/channels/" + channel.name;
|
||||
asyncClient.getChannels(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
state.server_error = err.message;
|
||||
state.inValid = true;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
displayNameKeyUp: function(e) {
|
||||
var display_name = this.refs.display_name.getDOMNode().value.trim();
|
||||
var channel_name = utils.cleanUpUrlable(display_name);
|
||||
this.refs.channel_name.getDOMNode().value = channel_name;
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({ channel_type: $(button).attr('data-channeltype') });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { channel_type: "" };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var display_name_error = this.state.display_name_error ? <label className='control-label'>{ this.state.display_name_error }</label> : null;
|
||||
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" id="new_channel" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">New Channel</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form role="form">
|
||||
<div className={ this.state.display_name_error ? "form-group has-error" : "form-group" }>
|
||||
<label className='control-label'>Display Name</label>
|
||||
<input onKeyUp={this.displayNameKeyUp} type="text" ref="display_name" className="form-control" placeholder="Enter display name" maxLength="64" />
|
||||
{ display_name_error }
|
||||
</div>
|
||||
<div className={ this.state.name_error ? "form-group has-error" : "form-group" }>
|
||||
<label className='control-label'>Handle</label>
|
||||
<input type="text" className="form-control" ref="channel_name" placeholder="lowercase alphanumeric's only" maxLength="64" />
|
||||
{ name_error }
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className='control-label'>Description</label>
|
||||
<textarea className="form-control" ref="channel_desc" rows="3" placeholder="Description" maxLength="1024"></textarea>
|
||||
</div>
|
||||
{ server_error }
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button onClick={this.handleSubmit} type="button" className="btn btn-primary">Create New Channel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
178
web/react/components/password_reset.jsx
Обычный файл
178
web/react/components/password_reset.jsx
Обычный файл
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
SendResetPasswordLink = React.createClass({
|
||||
handleSendLink: function(e) {
|
||||
e.preventDefault();
|
||||
var state = {};
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim();
|
||||
if (!email) {
|
||||
state.error = "Please enter a valid email address."
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.error = null;
|
||||
this.setState(state);
|
||||
|
||||
data = {};
|
||||
data['email'] = email;
|
||||
data['domain'] = domain;
|
||||
|
||||
client.sendPasswordReset(data,
|
||||
function(data) {
|
||||
this.setState({ error: null, update_text: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamName}</b> team on {config.SiteName}.com.</p>, more_update_text: "Please check your inbox." });
|
||||
$(this.refs.reset_form.getDOMNode()).hide();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ error: err.message, update_text: null, more_update_text: null });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
render: function() {
|
||||
var update_text = this.state.update_text ? <div className="reset-form alert alert-success">{this.state.update_text}{this.state.more_update_text}</div> : null;
|
||||
var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null;
|
||||
|
||||
var subDomain = utils.getSubDomain();
|
||||
var subDomainClass = "form-control hidden";
|
||||
|
||||
if (subDomain == "") {
|
||||
subDomain = UserStore.getLastDomain();
|
||||
subDomainClass = "form-control";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-sm-12">
|
||||
<div className="signup-team__container">
|
||||
<h3>Password Reset</h3>
|
||||
{ update_text }
|
||||
<form onSubmit={this.handleSendLink} ref="reset_form">
|
||||
<p>{"To reset your password, enter the email address you used to sign up for " + this.props.teamName + "."}</p>
|
||||
<div className="form-group">
|
||||
<input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" />
|
||||
</div>
|
||||
<div className={error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="text" className="form-control" name="email" ref="email" placeholder="Email" />
|
||||
</div>
|
||||
{ error }
|
||||
<button type="submit" className="btn btn-primary">Reset my password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ResetPassword = React.createClass({
|
||||
handlePasswordReset: function(e) {
|
||||
e.preventDefault();
|
||||
var state = {};
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!password || password.length < 5) {
|
||||
state.error = "Please enter at least 5 characters."
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.error = null;
|
||||
this.setState(state);
|
||||
|
||||
data = {};
|
||||
data['new_password'] = password;
|
||||
data['hash'] = this.props.hash;
|
||||
data['data'] = this.props.data;
|
||||
data['domain'] = domain;
|
||||
|
||||
client.resetPassword(data,
|
||||
function(data) {
|
||||
this.setState({ error: null, update_text: "Your password has been updated successfully." });
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({ error: err.message, update_text: null });
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
render: function() {
|
||||
var update_text = this.state.update_text ? <div className="form-group"><br/><label className="control-label reset-form">{this.state.update_text} Click <a href="/login">here</a> to log in.</label></div> : null;
|
||||
var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null;
|
||||
|
||||
var subDomain = this.props.domain != "" ? this.props.domain : utils.getSubDomain();
|
||||
var subDomainClass = "form-control hidden";
|
||||
|
||||
if (subDomain == "") {
|
||||
subDomain = UserStore.getLastDomain();
|
||||
subDomainClass = "form-control";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-sm-12">
|
||||
<div className="signup-team__container">
|
||||
<h3>Password Reset</h3>
|
||||
<form onSubmit={this.handlePasswordReset}>
|
||||
<p>{"Enter a new password for your " + this.props.teamName + " " + config.SiteName + " account."}</p>
|
||||
<div className="form-group">
|
||||
<input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" />
|
||||
</div>
|
||||
<div className={error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="password" className="form-control" name="password" ref="password" placeholder="Password" />
|
||||
</div>
|
||||
{ error }
|
||||
<button type="submit" className="btn btn-primary">Change my password</button>
|
||||
{ update_text }
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
render: function() {
|
||||
|
||||
if (this.props.isReset === "false") {
|
||||
return (
|
||||
<SendResetPasswordLink
|
||||
teamName={this.props.teamName}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ResetPassword
|
||||
teamName={this.props.teamName}
|
||||
domain={this.props.domain}
|
||||
hash={this.props.hash}
|
||||
data={this.props.data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
88
web/react/components/post.jsx
Обычный файл
88
web/react/components/post.jsx
Обычный файл
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostHeader = require('./post_header.jsx');
|
||||
var PostBody = require('./post_body.jsx');
|
||||
var PostInfo = require('./post_info.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
$('.edit-modal').on('show.bs.modal', function () {
|
||||
$('.edit-modal .edit-modal-body').css('overflow-y', 'auto');
|
||||
$('.edit-modal .edit-modal-body').css('max-height', $(window).height() * 0.7);
|
||||
});
|
||||
},
|
||||
handleCommentClick: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
data = {};
|
||||
data.order = [this.props.post.id];
|
||||
data.posts = this.props.posts;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: data
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var post = this.props.post;
|
||||
var parentPost = this.props.parentPost;
|
||||
var posts = this.props.posts;
|
||||
|
||||
var type = "Post"
|
||||
if (post.root_id.length > 0) {
|
||||
type = "Comment"
|
||||
}
|
||||
|
||||
var commentCount = 0;
|
||||
var commentRootId = parentPost ? post.root_id : post.id;
|
||||
var rootUser = "";
|
||||
for (var postId in posts) {
|
||||
if (posts[postId].root_id == commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null;
|
||||
|
||||
if(this.props.sameRoot){
|
||||
rootUser = "same--root";
|
||||
}
|
||||
else {
|
||||
rootUser = "other--root";
|
||||
}
|
||||
|
||||
var postType = "";
|
||||
if(type != "Post"){
|
||||
postType = "post--comment";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType}>
|
||||
{ !this.props.hideProfilePic ?
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
: "" }
|
||||
<div className="post__content">
|
||||
<PostHeader post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} />
|
||||
<PostBody post={post} sameRoot={this.props.sameRoot} parentPost={parentPost} posts={posts} handleCommentClick={this.handleCommentClick} />
|
||||
<PostInfo post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
141
web/react/components/post_body.jsx
Обычный файл
141
web/react/components/post_body.jsx
Обычный файл
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var CreateComment = require( './create_comment.jsx' );
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var ViewImageModal = require('./view_image.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleImageClick: function(e) {
|
||||
this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))});
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var linkData = utils.extractLinks(nextProps.post.message);
|
||||
this.setState({ links: linkData["links"], message: linkData["text"] });
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var filenames = this.props.post.filenames;
|
||||
var self = this;
|
||||
if (filenames) {
|
||||
var re1 = new RegExp(' ', 'g');
|
||||
var re2 = new RegExp('\\(', 'g');
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
var fileInfo = utils.splitFileLocation(filenames[i]);
|
||||
if (Object.keys(fileInfo).length === 0) continue;
|
||||
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
|
||||
if (type === "image") {
|
||||
$('<img/>').attr('src', fileInfo.path+'_thumb.jpg').load(function(path, name){ return function() {
|
||||
$(this).remove();
|
||||
if (name in self.refs) {
|
||||
var imgDiv = self.refs[name].getDOMNode();
|
||||
$(imgDiv).removeClass('post__load');
|
||||
$(imgDiv).addClass('post__image');
|
||||
var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
$(imgDiv).css('background-image', 'url('+url+'_thumb.jpg)');
|
||||
}
|
||||
}}(fileInfo.path, filenames[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
var linkData = utils.extractLinks(this.props.post.message);
|
||||
return { startImgId: 0, links: linkData["links"], message: linkData["text"] };
|
||||
},
|
||||
render: function() {
|
||||
var post = this.props.post;
|
||||
var filenames = this.props.post.filenames;
|
||||
var parentPost = this.props.parentPost;
|
||||
var postImageModalId = "view_image_modal_" + post.id;
|
||||
var inner = utils.textToJsx(this.state.message);
|
||||
|
||||
var comment = "";
|
||||
var reply = "";
|
||||
var postClass = "";
|
||||
|
||||
if (parentPost) {
|
||||
var profile = UserStore.getProfile(parentPost.user_id);
|
||||
var apostrophe = "";
|
||||
var name = "...";
|
||||
if (profile != null) {
|
||||
if (profile.username.slice(-1) === 's') {
|
||||
apostrophe = "'";
|
||||
} else {
|
||||
apostrophe = "'s";
|
||||
}
|
||||
name = <a className="theme" onClick={function(){ utils.searchForTerm(profile.username); }}>{profile.username}</a>;
|
||||
}
|
||||
|
||||
var message = parentPost.message;
|
||||
|
||||
comment = (
|
||||
<p className="post-link">
|
||||
<span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{utils.replaceHtmlEntities(message)}</a></span>
|
||||
</p>
|
||||
);
|
||||
|
||||
postClass += " post-comment";
|
||||
}
|
||||
|
||||
var postFiles = [];
|
||||
var images = [];
|
||||
if (filenames) {
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
var fileInfo = utils.splitFileLocation(filenames[i]);
|
||||
if (Object.keys(fileInfo).length === 0) continue;
|
||||
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
|
||||
if (type === "image") {
|
||||
postFiles.push(
|
||||
<div className="post-image__column" key={filenames[i]}>
|
||||
<a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filenames[i]} className="post__load" style={{backgroundImage: 'url(/static/images/load.gif)'}}></div></a>
|
||||
</div>
|
||||
);
|
||||
images.push(filenames[i]);
|
||||
} else {
|
||||
postFiles.push(
|
||||
<div className="post-image__column custom-file" key={fileInfo.name}>
|
||||
<a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}>
|
||||
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var embed;
|
||||
if (postFiles.length === 0 && this.state.links) {
|
||||
embed = utils.getEmbed(this.state.links[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post-body">
|
||||
{ comment }
|
||||
<p key={post.Id+"_message"} className={postClass}><span>{inner}</span></p>
|
||||
{ filenames && filenames.length > 0 ?
|
||||
<div className="post-image__columns">
|
||||
{ postFiles }
|
||||
</div>
|
||||
: "" }
|
||||
{ embed }
|
||||
|
||||
{ images.length > 0 ?
|
||||
<ViewImageModal
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
modalId={postImageModalId}
|
||||
startId={this.state.startImgId}
|
||||
imgCount={post.img_count}
|
||||
filenames={images} />
|
||||
: "" }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
36
web/react/components/post_deleted_modal.jsx
Обычный файл
36
web/react/components/post_deleted_modal.jsx
Обычный файл
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var currentUser = UserStore.getCurrentUser()
|
||||
|
||||
if (currentUser != null) {
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="post_deleted" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" id="myModalLabel">Comment could not be posted</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Someone deleted the message on which you tried to post a comment.</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-primary" data-dismiss="modal">Agree</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
});
|
||||
23
web/react/components/post_header.jsx
Обычный файл
23
web/react/components/post_header.jsx
Обычный файл
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserProfile = require( './user_profile.jsx' );
|
||||
var PostInfo = require('./post_info.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var post = this.props.post;
|
||||
|
||||
return (
|
||||
<ul className="post-header post-header-post">
|
||||
<li className="post-header-col post-header__name"><strong><UserProfile userId={post.user_id} /></strong></li>
|
||||
<li className="post-info--hidden">
|
||||
<PostInfo post={post} commentCount={this.props.commentCount} handleCommentClick={this.props.handleCommentClick} allowReply="true" isLastComment={this.props.isLastComment} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
52
web/react/components/post_info.jsx
Обычный файл
52
web/react/components/post_info.jsx
Обычный файл
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var post = this.props.post;
|
||||
var isOwner = UserStore.getCurrentId() == post.user_id;
|
||||
|
||||
var type = "Post"
|
||||
if (post.root_id.length > 0) {
|
||||
type = "Comment"
|
||||
}
|
||||
|
||||
var comments = "";
|
||||
var lastCommentClass = this.props.isLastComment ? " comment-icon__container__show" : " comment-icon__container__hide";
|
||||
if (this.props.commentCount >= 1) {
|
||||
comments = <a href="#" className={"comment-icon__container theme" + lastCommentClass} onClick={this.props.handleCommentClick}><span className="comment-icon" dangerouslySetInnerHTML={{__html: "<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>"}} />{this.props.commentCount}</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="post-header post-info">
|
||||
<li className="post-header-col"><time className="post-profile-time">{ utils.displayDateTime(post.create_at) }</time></li>
|
||||
<li className="post-header-col post-header__reply">
|
||||
<div className="dropdown">
|
||||
{ isOwner || (this.props.allowReply === "true" && type != "Comment") ?
|
||||
<div>
|
||||
<a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
[...]
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
{ isOwner ? <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id} data-comments={type === "Post" ? this.props.commentCount : 0}>Edit</a></li>
|
||||
: "" }
|
||||
{ isOwner ? <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={type === "Post" ? this.props.commentCount : 0}>Delete</a></li>
|
||||
: "" }
|
||||
{ this.props.allowReply === "true" ? <li role="presentation"><a className="reply-link theme" href="#" onClick={this.props.handleCommentClick}>Reply</a></li>
|
||||
: "" }
|
||||
</ul>
|
||||
</div>
|
||||
: "" }
|
||||
</div>
|
||||
{ comments }
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
474
web/react/components/post_list.jsx
Обычный файл
474
web/react/components/post_list.jsx
Обычный файл
@@ -0,0 +1,474 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var UserProfile = require( './user_profile.jsx' );
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var CreatePost = require('./create_post.jsx');
|
||||
var Post = require('./post.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getStateFromStores() {
|
||||
var channel = ChannelStore.getCurrent();
|
||||
|
||||
if (channel == null) channel = {};
|
||||
|
||||
return {
|
||||
post_list: PostStore.getCurrentPosts(),
|
||||
channel: channel
|
||||
};
|
||||
}
|
||||
|
||||
function changeColor(col, amt) {
|
||||
|
||||
var usePound = false;
|
||||
|
||||
if (col[0] == "#") {
|
||||
col = col.slice(1);
|
||||
usePound = true;
|
||||
}
|
||||
|
||||
var num = parseInt(col,16);
|
||||
|
||||
var r = (num >> 16) + amt;
|
||||
|
||||
if (r > 255) r = 255;
|
||||
else if (r < 0) r = 0;
|
||||
|
||||
var b = ((num >> 8) & 0x00FF) + amt;
|
||||
|
||||
if (b > 255) b = 255;
|
||||
else if (b < 0) b = 0;
|
||||
|
||||
var g = (num & 0x0000FF) + amt;
|
||||
|
||||
if (g > 255) g = 255;
|
||||
else if (g < 0) g = 0;
|
||||
|
||||
return (usePound?"#":"") + String("000000" + (g | (b << 8) | (r << 16)).toString(16)).slice(-6);
|
||||
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
scrollPosition: 0,
|
||||
preventScrollTrigger: false,
|
||||
gotMorePosts: false,
|
||||
oldScrollHeight: 0,
|
||||
oldZoom: 0,
|
||||
scrolledToNew: false,
|
||||
componentDidMount: function() {
|
||||
var user = UserStore.getCurrentUser();
|
||||
if (user.props && user.props.theme) {
|
||||
utils.changeCss('a.theme', 'color:'+user.props.theme+'; fill:'+user.props.theme+'!important;');
|
||||
utils.changeCss('div.theme', 'background-color:'+user.props.theme+';');
|
||||
utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme+';');
|
||||
utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + changeColor(user.props.theme, -10) +';');
|
||||
utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme+';');
|
||||
utils.changeCss('.mention', 'background: ' + user.props.theme+';');
|
||||
utils.changeCss('.mention-link', 'color: ' + user.props.theme+';');
|
||||
utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme+';}');
|
||||
}
|
||||
|
||||
PostStore.addChangeListener(this._onChange);
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
SocketStore.addChangeListener(this._onSocketChange);
|
||||
|
||||
$(".post-list-holder-by-time").perfectScrollbar();
|
||||
|
||||
this.resize();
|
||||
|
||||
var post_holder = $(".post-list-holder-by-time")[0];
|
||||
this.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight();
|
||||
this.oldScrollHeight = post_holder.scrollHeight;
|
||||
this.oldZoom = (window.outerWidth - 8) / window.innerWidth;
|
||||
|
||||
var self = this;
|
||||
$(window).resize(function(){
|
||||
$(post_holder).perfectScrollbar('update');
|
||||
|
||||
// this only kind of works, detecting zoom in browsers is a nightmare
|
||||
var newZoom = (window.outerWidth - 8) / window.innerWidth;
|
||||
|
||||
if (self.scrollPosition >= post_holder.scrollHeight || (self.oldScrollHeight != post_holder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom != newZoom) self.resize();
|
||||
|
||||
self.oldZoom = newZoom;
|
||||
|
||||
if ($('#create_post').length > 0) {
|
||||
var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
|
||||
$(".post-list-holder-by-time").css("height", height + "px");
|
||||
}
|
||||
});
|
||||
|
||||
$(post_holder).scroll(function(e){
|
||||
if (!self.preventScrollTrigger) {
|
||||
self.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight();
|
||||
}
|
||||
self.preventScrollTrigger = false;
|
||||
});
|
||||
|
||||
$('body').on('click.userpopover', function(e){
|
||||
if ($(e.target).attr('data-toggle') !== 'popover'
|
||||
&& $(e.target).parents('.popover.in').length === 0) {
|
||||
$('.user-popover').popover('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$('.post-list__content div .post').removeClass('post--last');
|
||||
$('.post-list__content div:last-child .post').addClass('post--last');
|
||||
|
||||
$('body').on('mouseenter mouseleave', '.post', function(ev){
|
||||
if(ev.type === 'mouseenter'){
|
||||
$(this).parent('div').prev('.date-seperator').addClass('hovered--after');
|
||||
$(this).parent('div').next('.date-seperator').addClass('hovered--before');
|
||||
}
|
||||
else {
|
||||
$(this).parent('div').prev('.date-seperator').removeClass('hovered--after');
|
||||
$(this).parent('div').next('.date-seperator').removeClass('hovered--before');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.resize();
|
||||
var post_holder = $(".post-list-holder-by-time")[0];
|
||||
this.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight();
|
||||
this.oldScrollHeight = post_holder.scrollHeight;
|
||||
$('.post-list__content div .post').removeClass('post--last');
|
||||
$('.post-list__content div:last-child .post').addClass('post--last');
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeChangeListener(this._onChange);
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
SocketStore.removeChangeListener(this._onSocketChange);
|
||||
$('body').off('click.userpopover');
|
||||
},
|
||||
resize: function() {
|
||||
if (this.gotMorePosts) {
|
||||
this.gotMorePosts = false;
|
||||
var post_holder = $(".post-list-holder-by-time")[0];
|
||||
this.preventScrollTrigger = true;
|
||||
$(post_holder).scrollTop($(post_holder).scrollTop() + (post_holder.scrollHeight-this.oldScrollHeight) );
|
||||
$(post_holder).perfectScrollbar('update');
|
||||
} else {
|
||||
var post_holder = $(".post-list-holder-by-time")[0];
|
||||
this.preventScrollTrigger = true;
|
||||
if ($("#new_message")[0] && !this.scrolledToNew) {
|
||||
$(post_holder).scrollTop($(post_holder).scrollTop() + $("#new_message").offset().top - 63);
|
||||
$(post_holder).perfectScrollbar('update');
|
||||
this.scrolledToNew = true;
|
||||
} else {
|
||||
$(post_holder).scrollTop(post_holder.scrollHeight);
|
||||
$(post_holder).perfectScrollbar('update');
|
||||
}
|
||||
}
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
if (this.state.post_list && this.state.post_list.order) {
|
||||
if (this.state.channel.id === newState.channel.id && this.state.post_list.order.length != newState.post_list.order.length && newState.post_list.order.length > Constants.POST_CHUNK_SIZE) {
|
||||
this.gotMorePosts = true;
|
||||
}
|
||||
}
|
||||
if (this.state.channel.id !== newState.channel.id) {
|
||||
this.scrolledToNew = false;
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
_onSocketChange: function(msg) {
|
||||
if (msg.action == "posted") {
|
||||
var post = JSON.parse(msg.props.post);
|
||||
|
||||
var post_list = PostStore.getPosts(msg.channel_id);
|
||||
if (!post_list) return;
|
||||
|
||||
post_list.posts[post.id] = post;
|
||||
if (post_list.order.indexOf(post.id) === -1) {
|
||||
post_list.order.unshift(post.id);
|
||||
}
|
||||
|
||||
if (this.state.channel.id === msg.channel_id) {
|
||||
this.setState({ post_list: post_list });
|
||||
};
|
||||
|
||||
PostStore.storePosts(post.channel_id, post_list);
|
||||
} else if (msg.action == "post_edited") {
|
||||
if (this.state.channel.id == msg.channel_id) {
|
||||
var post_list = this.state.post_list;
|
||||
if (!(msg.props.post_id in post_list.posts)) return;
|
||||
|
||||
var post = post_list.posts[msg.props.post_id];
|
||||
post.message = msg.props.message;
|
||||
|
||||
post_list.posts[post.id] = post;
|
||||
this.setState({ post_list: post_list });
|
||||
|
||||
PostStore.storePosts(msg.channel_id, post_list);
|
||||
} else {
|
||||
AsyncClient.getPosts(true, msg.channel_id);
|
||||
}
|
||||
} else if (msg.action == "post_deleted") {
|
||||
var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
|
||||
var activeRootPostId = activeRoot && activeRoot.id.length > 0 ? activeRoot.id : "";
|
||||
|
||||
if (this.state.channel.id == msg.channel_id) {
|
||||
var post_list = this.state.post_list;
|
||||
if (!(msg.props.post_id in this.state.post_list.posts)) return;
|
||||
|
||||
delete post_list.posts[msg.props.post_id];
|
||||
var index = post_list.order.indexOf(msg.props.post_id);
|
||||
if (index > -1) post_list.order.splice(index, 1);
|
||||
|
||||
var scrollSave = $(".post-list-holder-by-time").scrollTop();
|
||||
|
||||
this.setState({ post_list: post_list });
|
||||
|
||||
$(".post-list-holder-by-time").scrollTop(scrollSave)
|
||||
|
||||
PostStore.storePosts(msg.channel_id, post_list);
|
||||
} else {
|
||||
AsyncClient.getPosts(true, msg.channel_id);
|
||||
}
|
||||
|
||||
if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() != msg.user_id) {
|
||||
$('#post_deleted').modal('show');
|
||||
}
|
||||
} else if(msg.action == "new_user") {
|
||||
AsyncClient.getProfiles();
|
||||
}
|
||||
},
|
||||
getMorePosts: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.post_list) return;
|
||||
|
||||
var posts = this.state.post_list.posts;
|
||||
var order = this.state.post_list.order;
|
||||
var channel_id = this.state.channel.id;
|
||||
|
||||
$(this.refs.loadmore.getDOMNode()).text("Retrieving more messages...");
|
||||
|
||||
var self = this;
|
||||
var currentPos = $(".post-list").scrollTop;
|
||||
|
||||
Client.getPosts(
|
||||
channel_id,
|
||||
order.length,
|
||||
Constants.POST_CHUNK_SIZE,
|
||||
function(data) {
|
||||
$(self.refs.loadmore.getDOMNode()).text("Load more messages");
|
||||
|
||||
if (!data) return;
|
||||
|
||||
if (data.order.length === 0) return;
|
||||
|
||||
var post_list = {}
|
||||
post_list.posts = $.extend(posts, data.posts);
|
||||
post_list.order = order.concat(data.order);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POSTS,
|
||||
id: channel_id,
|
||||
post_list: post_list
|
||||
});
|
||||
|
||||
Client.getProfiles();
|
||||
$(".post-list").scrollTop(currentPos);
|
||||
},
|
||||
function(err) {
|
||||
$(self.refs.loadmore.getDOMNode()).text("Load more messages");
|
||||
dispatchError(err, "getPosts");
|
||||
}
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var order = [];
|
||||
var posts = {};
|
||||
|
||||
var last_viewed = Number.MAX_VALUE;
|
||||
|
||||
if (ChannelStore.getCurrentMember() != null)
|
||||
last_viewed = ChannelStore.getCurrentMember().last_viewed_at;
|
||||
|
||||
if (this.state.post_list != null) {
|
||||
posts = this.state.post_list.posts;
|
||||
order = this.state.post_list.order;
|
||||
}
|
||||
|
||||
var rendered_last_viewed = false;
|
||||
|
||||
var user_id = "";
|
||||
if (UserStore.getCurrentId()) {
|
||||
user_id = UserStore.getCurrentId();
|
||||
} else {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
var channel = this.state.channel;
|
||||
|
||||
var more_messages = <p className="beginning-messages-text">Beginning of Channel</p>;
|
||||
|
||||
if (channel != null) {
|
||||
if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) {
|
||||
more_messages = <a ref="loadmore" className="more-messages-text theme" href="#" onClick={this.getMorePosts}>Load more messages</a>;
|
||||
} else if (channel.type === 'D') {
|
||||
var userIds = channel.name.split('__');
|
||||
var teammate;
|
||||
if (userIds.length === 2 && userIds[0] === user_id) {
|
||||
teammate = UserStore.getProfile(userIds[1]);
|
||||
} else if (userIds.length === 2 && userIds[1] === user_id) {
|
||||
teammate = UserStore.getProfile(userIds[0]);
|
||||
}
|
||||
|
||||
if (teammate) {
|
||||
var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username;
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<div className="post-profile-img__container channel-intro-img">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + teammate.id + "/image"} height="50" width="50" />
|
||||
</div>
|
||||
<div className="channel-intro-profile">
|
||||
<strong><UserProfile userId={teammate.id} /></strong>
|
||||
</div>
|
||||
<p className="channel-intro-text">{"This is the start of your direct message history with " + teammate_name + "." }<br/>{"Direct messages and files shared here are not shown to people outside this area."}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<p className="channel-intro-text">{"This is the start of your direct message history with this " + strings.Team + "mate. Direct messages and files shared here are not shown to people outside this area."}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (channel.type === 'P' || channel.type === 'O') {
|
||||
var ui_name = channel.display_name
|
||||
var members = ChannelStore.getCurrentExtraInfo().members;
|
||||
var creator_name = "";
|
||||
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
if (members[i].roles.indexOf('admin') > -1) {
|
||||
creator_name = members[i].username;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.name === Constants.DEFAULT_CHANNEL) {
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<h4 className="channel-intro-title">Welcome</h4>
|
||||
<p>
|
||||
Welcome to {ui_name}!
|
||||
<br/><br/>
|
||||
{"This is the first channel " + strings.Team + "mates see when they"}
|
||||
<br/>
|
||||
sign up - use it for posting updates everyone needs to know.
|
||||
<br/><br/>
|
||||
To create a new channel or join an existing one, go to
|
||||
<br/>
|
||||
the Left Hand Sidebar under “Channels” and click “More…”.
|
||||
<br/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var userStyle = { color: UserStore.getCurrentUser().props.theme }
|
||||
var ui_type = channel.type === 'P' ? "private group" : "channel";
|
||||
more_messages = (
|
||||
<div className="channel-intro">
|
||||
<h4 className="channel-intro-title">Welcome</h4>
|
||||
<p>
|
||||
{ creator_name != "" ? "This is the start of the " + ui_name + " " + ui_type + ", created by " + creator_name + " on " + utils.displayDate(channel.create_at) + "."
|
||||
: "This is the start of the " + ui_name + " " + ui_type + ", created on "+ utils.displayDate(channel.create_at) + "." }
|
||||
{ channel.type === 'P' ? " Only invited members can see this private group." : " Any member can join and read this channel." }
|
||||
<br/>
|
||||
<a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a>
|
||||
<a className="intro-links" style={userStyle} data-toggle="modal" data-target="#channel_invite"><i className="fa fa-user-plus"></i>Invite others to this {ui_type}</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var postCtls = [];
|
||||
var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
|
||||
var currentPostDay = new Date();
|
||||
|
||||
for (var i = order.length-1; i >= 0; i--) {
|
||||
var post = posts[order[i]];
|
||||
var parentPost;
|
||||
|
||||
if (post.parent_id) {
|
||||
parentPost = posts[post.parent_id];
|
||||
} else {
|
||||
parentPost = null;
|
||||
}
|
||||
|
||||
var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
|
||||
var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
|
||||
|
||||
// we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
|
||||
var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
var isLastComment = false;
|
||||
if (utils.isComment(post)) {
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
|
||||
}
|
||||
|
||||
var postCtl = <Post sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />;
|
||||
|
||||
currentPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
|
||||
postCtls.push(
|
||||
<div className="date-seperator">
|
||||
<hr className="date-seperator__hr" />
|
||||
<div className="date-seperator__text">{currentPostDay.toDateString()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.create_at > last_viewed && !rendered_last_viewed) {
|
||||
rendered_last_viewed = true;
|
||||
postCtls.push(
|
||||
<div>
|
||||
<div className="new-seperator">
|
||||
<hr id="new_message" className="new-seperator__hr" />
|
||||
<div className="new-seperator__text">New Messages</div>
|
||||
</div>
|
||||
{postCtl}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
postCtls.push(postCtl);
|
||||
}
|
||||
previousPostDay = utils.getDateForUnixTicks(post.create_at);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="postlist" className="post-list-holder-by-time">
|
||||
<div className="post-list__table">
|
||||
<div className="post-list__content">
|
||||
{ more_messages }
|
||||
{ postCtls }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
397
web/react/components/post_right.jsx
Обычный файл
397
web/react/components/post_right.jsx
Обычный файл
@@ -0,0 +1,397 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserProfile = require( './user_profile.jsx' );
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var SearchBox =require('./search_bar.jsx');
|
||||
var CreateComment = require( './create_comment.jsx' );
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ViewImageModal = require('./view_image.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
RhsHeaderPost = React.createClass({
|
||||
handleClose: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
},
|
||||
handleBack: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH_TERM,
|
||||
term: this.props.fromSearch,
|
||||
do_search: true,
|
||||
is_mention_search: this.props.isMentionSearch
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} style={{color:"black"}}>{"< "}</a> : "";
|
||||
|
||||
return (
|
||||
<div className="sidebar--right__header">
|
||||
<span className="sidebar--right__title">{back}Message Details</span>
|
||||
<button type="button" className="sidebar--right__close" aria-label="Close" onClick={this.handleClose}></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
RootPost = React.createClass({
|
||||
handleImageClick: function(e) {
|
||||
this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { startImgId: 0 };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var postImageModalId = "rhs_view_image_modal_" + this.props.post.id;
|
||||
var message = utils.textToJsx(this.props.post.message);
|
||||
var filenames = this.props.post.filenames;
|
||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||
|
||||
var type = "Post"
|
||||
if (this.props.post.root_id.length > 0) {
|
||||
type = "Comment"
|
||||
}
|
||||
|
||||
if (filenames) {
|
||||
var postFiles = [];
|
||||
var images = [];
|
||||
var re1 = new RegExp(' ', 'g');
|
||||
var re2 = new RegExp('\\(', 'g');
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
var fileSplit = filenames[i].split('.');
|
||||
if (fileSplit.length < 2) continue;
|
||||
|
||||
var ext = fileSplit[fileSplit.length-1];
|
||||
fileSplit.splice(fileSplit.length-1,1)
|
||||
var filePath = fileSplit.join('.');
|
||||
var filename = filePath.split('/')[filePath.split('/').length-1];
|
||||
|
||||
var ftype = utils.getFileType(ext);
|
||||
|
||||
if (ftype === "image") {
|
||||
var url = filePath.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
postFiles.push(
|
||||
<div className="post-image__column" key={filePath}>
|
||||
<a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filePath} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a>
|
||||
</div>
|
||||
);
|
||||
images.push(filenames[i]);
|
||||
} else {
|
||||
postFiles.push(
|
||||
<div className="post-image__column custom-file" key={filePath}>
|
||||
<a href={filePath+"."+ext} download={filename+"."+ext}>
|
||||
<div className={"file-icon "+utils.getIconClassName(ftype)}/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post post--root">
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
<div className="post__content">
|
||||
<ul className="post-header">
|
||||
<li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
|
||||
<li className="post-header-col"><time className="post-right-root-time">{ utils.displayDate(this.props.post.create_at)+' '+utils.displayTime(this.props.post.create_at) }</time></li>
|
||||
<li className="post-header-col post-header__reply">
|
||||
<div className="dropdown">
|
||||
{ isOwner ?
|
||||
<div>
|
||||
<a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
[...]
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
<li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li>
|
||||
<li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={this.props.commentCount}>Delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
: "" }
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="post-body">
|
||||
<p>{message}</p>
|
||||
{ filenames.length > 0 ?
|
||||
<div className="post-image__columns">
|
||||
{ postFiles }
|
||||
</div>
|
||||
: "" }
|
||||
{ images.length > 0 ?
|
||||
<ViewImageModal
|
||||
channelId={this.props.post.channel_id}
|
||||
userId={this.props.post.user_id}
|
||||
modalId={postImageModalId}
|
||||
startId={this.state.startImgId}
|
||||
imgCount={this.props.post.img_count}
|
||||
filenames={images} />
|
||||
: "" }
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
CommentPost = React.createClass({
|
||||
handleImageClick: function(e) {
|
||||
this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { startImgId: 0 };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var commentClass = "post";
|
||||
|
||||
var postImageModalId = "rhs_comment_view_image_modal_" + this.props.post.id;
|
||||
var filenames = this.props.post.filenames;
|
||||
var isOwner = UserStore.getCurrentId() == this.props.post.user_id;
|
||||
|
||||
var type = "Post"
|
||||
if (this.props.post.root_id.length > 0) {
|
||||
type = "Comment"
|
||||
}
|
||||
|
||||
if (filenames) {
|
||||
var postFiles = [];
|
||||
var images = [];
|
||||
var re1 = new RegExp(' ', 'g');
|
||||
var re2 = new RegExp('\\(', 'g');
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
var fileSplit = filenames[i].split('.');
|
||||
if (fileSplit.length < 2) continue;
|
||||
|
||||
var ext = fileSplit[fileSplit.length-1];
|
||||
fileSplit.splice(fileSplit.length-1,1)
|
||||
var filePath = fileSplit.join('.');
|
||||
var filename = filePath.split('/')[filePath.split('/').length-1];
|
||||
|
||||
var type = utils.getFileType(ext);
|
||||
|
||||
if (type === "image") {
|
||||
var url = filePath.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
postFiles.push(
|
||||
<div className="post-image__column" key={filename}>
|
||||
<a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filename} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a>
|
||||
</div>
|
||||
);
|
||||
images.push(filenames[i]);
|
||||
} else {
|
||||
postFiles.push(
|
||||
<div className="post-image__column custom-file" key={filename}>
|
||||
<a href={filePath+"."+ext} download={filename+"."+ext}>
|
||||
<div className={"file-icon "+utils.getIconClassName(type)}/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var message = utils.textToJsx(this.props.post.message);
|
||||
|
||||
return (
|
||||
<div className={commentClass}>
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
<div className="post__content">
|
||||
<ul className="post-header">
|
||||
<li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
|
||||
<li className="post-header-col"><time className="post-right-comment-time">{ utils.displayDateTime(this.props.post.create_at) }</time></li>
|
||||
<li className="post-header-col post-header__reply">
|
||||
{ isOwner ?
|
||||
<div className="dropdown" onClick={function(e){$('.post-list-holder-by-time').scrollTop($(".post-list-holder-by-time").scrollTop() + 50);}}>
|
||||
<a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
[...]
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
<li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li>
|
||||
<li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={0}>Delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
: "" }
|
||||
</li>
|
||||
</ul>
|
||||
<div className="post-body">
|
||||
<p>{message}</p>
|
||||
{ filenames.length > 0 ?
|
||||
<div className="post-image__columns">
|
||||
{ postFiles }
|
||||
</div>
|
||||
: "" }
|
||||
{ images.length > 0 ?
|
||||
<ViewImageModal
|
||||
channelId={this.props.post.channel_id}
|
||||
userId={this.props.post.user_id}
|
||||
modalId={postImageModalId}
|
||||
startId={this.state.startImgId}
|
||||
imgCount={this.props.post.img_count}
|
||||
filenames={images} />
|
||||
: "" }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getStateFromStores() {
|
||||
return { post_list: PostStore.getSelectedPost() };
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
PostStore.addSelectedPostChangeListener(this._onChange);
|
||||
PostStore.addChangeListener(this._onChangeAll);
|
||||
$(".post-right__scroll").perfectScrollbar();
|
||||
this.resize();
|
||||
var self = this;
|
||||
$(window).resize(function(){
|
||||
self.resize();
|
||||
});
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.resize();
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeSelectedPostChangeListener(this._onChange);
|
||||
PostStore.removeChangeListener(this._onChangeAll);
|
||||
},
|
||||
_onChange: function() {
|
||||
if (this.isMounted()) {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
_onChangeAll: function() {
|
||||
|
||||
if (this.isMounted()) {
|
||||
|
||||
// if something was changed in the channel like adding a
|
||||
// comment or post then lets refresh the sidebar list
|
||||
var currentSelected = PostStore.getSelectedPost();
|
||||
if (!currentSelected || currentSelected.order.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
|
||||
|
||||
if (!currentPosts || currentPosts.order.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (currentPosts.posts[currentPosts.order[0]].channel_id == currentSelected.posts[currentSelected.order[0]].channel_id) {
|
||||
currentSelected.posts = {};
|
||||
for (var postId in currentPosts.posts) {
|
||||
currentSelected.posts[postId] = currentPosts.posts[postId];
|
||||
}
|
||||
|
||||
PostStore.storeSelectedPost(currentSelected);
|
||||
}
|
||||
|
||||
this.setState(getStateFromStores());
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
resize: function() {
|
||||
var height = $(window).height() - $('#error_bar').outerHeight() - 100;
|
||||
$(".post-right__scroll").css("height", height + "px");
|
||||
$(".post-right__scroll").scrollTop(100000);
|
||||
$(".post-right__scroll").perfectScrollbar('update');
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var post_list = this.state.post_list;
|
||||
|
||||
if (post_list == null) {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
var selected_post = post_list.posts[post_list.order[0]];
|
||||
var root_post = null;
|
||||
|
||||
if (selected_post.root_id == "") {
|
||||
root_post = selected_post;
|
||||
}
|
||||
else {
|
||||
root_post = post_list.posts[selected_post.root_id];
|
||||
}
|
||||
|
||||
var posts_array = [];
|
||||
|
||||
for (var postId in post_list.posts) {
|
||||
var cpost = post_list.posts[postId];
|
||||
if (cpost.root_id == root_post.id) {
|
||||
posts_array.push(cpost);
|
||||
}
|
||||
}
|
||||
|
||||
posts_array.sort(function(a,b) {
|
||||
if (a.create_at < b.create_at)
|
||||
return -1;
|
||||
if (a.create_at > b.create_at)
|
||||
return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var results = this.state.results;
|
||||
var currentId = UserStore.getCurrentId();
|
||||
var searchForm = currentId == null ? null : <SearchBox />;
|
||||
|
||||
return (
|
||||
<div className="post-right__container">
|
||||
<div className="search-bar__container sidebar--right__search-header">{searchForm}</div>
|
||||
<div className="sidebar-right__body">
|
||||
<RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} />
|
||||
<div className="post-right__scroll">
|
||||
<RootPost post={root_post} commentCount={posts_array.length}/>
|
||||
<div className="post-right-comments-container">
|
||||
{ posts_array.map(function(cpost) {
|
||||
return <CommentPost key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } />
|
||||
})}
|
||||
</div>
|
||||
<div className="post-create__container">
|
||||
<CreateComment channelId={root_post.channel_id} rootId={root_post.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
142
web/react/components/rename_channel_modal.jsx
Обычный файл
142
web/react/components/rename_channel_modal.jsx
Обычный файл
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.channel_id.length !== 26) return;
|
||||
|
||||
var channel = ChannelStore.get(this.state.channel_id);
|
||||
var oldName = channel.name
|
||||
var oldDisplayName = channel.display_name
|
||||
var state = { server_error: "" };
|
||||
|
||||
channel.display_name = this.state.display_name.trim();
|
||||
if (!channel.display_name) {
|
||||
state.display_name_error = "This field is required";
|
||||
state.inValid = true;
|
||||
}
|
||||
else if (channel.display_name.length > 22) {
|
||||
state.display_name_error = "This field must be less than 22 characters";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.display_name_error = "";
|
||||
}
|
||||
|
||||
channel.name = this.state.channel_name.trim();
|
||||
if (!channel.name) {
|
||||
state.name_error = "This field is required";
|
||||
state.inValid = true;
|
||||
}
|
||||
else if(channel.name.length > 22){
|
||||
state.name_error = "This field must be less than 22 characters";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
var cleaned_name = utils.cleanUpUrlable(channel.name);
|
||||
if (cleaned_name != channel.name) {
|
||||
state.name_error = "Must be lowercase alphanumeric characters";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.name_error = "";
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (state.inValid)
|
||||
return;
|
||||
|
||||
if (oldName == channel.name && oldDisplayName == channel.display_name)
|
||||
return;
|
||||
|
||||
Client.updateChannel(channel,
|
||||
function(data) {
|
||||
this.refs.display_name.getDOMNode().value = "";
|
||||
this.refs.channel_name.getDOMNode().value = "";
|
||||
|
||||
$('#' + this.props.modalId).modal('hide');
|
||||
window.location.href = '/channels/' + this.state.channel_name;
|
||||
AsyncClient.getChannels(true);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
state.server_error = err.message;
|
||||
state.inValid = true;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
onNameChange: function() {
|
||||
this.setState({ channel_name: this.refs.channel_name.getDOMNode().value })
|
||||
},
|
||||
onDisplayNameChange: function() {
|
||||
this.setState({ display_name: this.refs.display_name.getDOMNode().value })
|
||||
},
|
||||
displayNameKeyUp: function(e) {
|
||||
var display_name = this.refs.display_name.getDOMNode().value.trim();
|
||||
var channel_name = utils.cleanUpUrlable(display_name);
|
||||
this.refs.channel_name.getDOMNode().value = channel_name;
|
||||
this.setState({ channel_name: channel_name })
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
var button = $(e.relatedTarget);
|
||||
self.setState({ display_name: button.attr('data-display'), title: button.attr('data-name'), channel_id: button.attr('data-channelid') });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { display_name: "", channel_name: "", channel_id: "" };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var display_name_error = this.state.display_name_error ? <label className='control-label'>{ this.state.display_name_error }</label> : null;
|
||||
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="rename_channel" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">Rename Channel</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form role="form">
|
||||
<div className={ this.state.display_name_error ? "form-group has-error" : "form-group" }>
|
||||
<label className='control-label'>Display Name</label>
|
||||
<input onKeyUp={this.displayNameKeyUp} onChange={this.onDisplayNameChange} type="text" ref="display_name" className="form-control" placeholder="Enter display name" value={this.state.display_name} maxLength="64" />
|
||||
{ display_name_error }
|
||||
</div>
|
||||
<div className={ this.state.name_error ? "form-group has-error" : "form-group" }>
|
||||
<label className='control-label'>Handle</label>
|
||||
<input onChange={this.onNameChange} type="text" className="form-control" ref="channel_name" placeholder="lowercase alphanumeric's only" value={this.state.channel_name} maxLength="64" />
|
||||
{ name_error }
|
||||
</div>
|
||||
{ server_error }
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button onClick={this.handleSubmit} type="button" className="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
92
web/react/components/rename_team_modal.jsx
Обычный файл
92
web/react/components/rename_team_modal.jsx
Обычный файл
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var state = { server_error: "" };
|
||||
var valid = true;
|
||||
|
||||
var name = this.state.name.trim();
|
||||
if (!name) {
|
||||
state.name_error = "This field is required";
|
||||
valid = false;
|
||||
} else {
|
||||
state.name_error = "";
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (!valid)
|
||||
return;
|
||||
|
||||
if (this.props.teamName === name)
|
||||
return;
|
||||
|
||||
var data = {};
|
||||
data["new_name"] = name;
|
||||
|
||||
Client.updateTeamName(data,
|
||||
function(data) {
|
||||
$('#rename_team_link').modal('hide');
|
||||
window.location.reload();
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
state.server_error = err.message;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
onNameChange: function() {
|
||||
this.setState({ name: this.refs.name.getDOMNode().value })
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) {
|
||||
self.setState({ name: self.props.teamName });
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { name: this.props.teamName };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="rename_team_link" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">{"Rename " + utils.toTitleCase(strings.Team)}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form role="form" onSubmit={this.handleSubmit}>
|
||||
<div className={ this.state.name_error ? "form-group has-error" : "form-group" }>
|
||||
<label className='control-label'>Name</label>
|
||||
<input onChange={this.onNameChange} type="text" ref="name" className="form-control" placeholder={"Enter "+strings.Team+" name"} value={this.state.name} maxLength="64" />
|
||||
{ name_error }
|
||||
</div>
|
||||
{ server_error }
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button onClick={this.handleSubmit} type="button" className="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
104
web/react/components/search_bar.jsx
Обычный файл
104
web/react/components/search_bar.jsx
Обычный файл
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
function getSearchTermStateFromStores() {
|
||||
term = PostStore.getSearchTerm();
|
||||
if (!term) term = "";
|
||||
return {
|
||||
search_term: term
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
PostStore.addSearchTermChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeSearchTermChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function(doSearch, isMentionSearch) {
|
||||
if (this.isMounted()) {
|
||||
var newState = getSearchTermStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
if (doSearch) {
|
||||
this.performSearch(newState.search_term, isMentionSearch);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleClose: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
},
|
||||
handleUserInput: function(e) {
|
||||
var term = e.target.value;
|
||||
PostStore.storeSearchTerm(term);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
this.setState({ search_term: term });
|
||||
},
|
||||
handleUserFocus: function(e) {
|
||||
e.target.select();
|
||||
},
|
||||
performSearch: function(terms, isMentionSearch) {
|
||||
if (terms.length > 0) {
|
||||
$("#search-spinner").removeClass("hidden");
|
||||
client.search(
|
||||
terms,
|
||||
function(data) {
|
||||
$("#search-spinner").addClass("hidden");
|
||||
if(utils.isMobile()) {
|
||||
$('#search')[0].value = "";
|
||||
}
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: data,
|
||||
is_mention_search: isMentionSearch
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
$("#search-spinner").addClass("hidden");
|
||||
dispatchError(err, "search");
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
terms = this.state.search_term.trim();
|
||||
this.performSearch(terms);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getSearchTermStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="sidebar__collapse" onClick={this.handleClose}></div>
|
||||
<span className="glyphicon glyphicon-search sidebar__search-icon"></span>
|
||||
<form role="form" className="search__form relative-div" onSubmit={this.handleSubmit}>
|
||||
<input type="text" className="form-control search-bar-box" ref="search" id="search" placeholder="Search" value={this.state.search_term} onFocus={this.handleUserFocus} onChange={this.handleUserInput} />
|
||||
<span id="search-spinner" className="glyphicon glyphicon-refresh glyphicon-refresh-animate hidden"></span>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
180
web/react/components/search_results.jsx
Обычный файл
180
web/react/components/search_results.jsx
Обычный файл
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var UserProfile = require( './user_profile.jsx' );
|
||||
var SearchBox =require('./search_bar.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client =require('../utils/client.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
RhsHeaderSearch = React.createClass({
|
||||
handleClose: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
results: null
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
var title = this.props.isMentionSearch ? "Recent Mentions" : "Search Results";
|
||||
return (
|
||||
<div className="sidebar--right__header">
|
||||
<span className="sidebar--right__title">{title}</span>
|
||||
<button type="button" className="sidebar--right__close" aria-label="Close" onClick={this.handleClose}></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
SearchItem = React.createClass({
|
||||
handleClick: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var self = this;
|
||||
client.getPost(
|
||||
this.props.post.channel_id,
|
||||
this.props.post.id,
|
||||
function(data) {
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: data,
|
||||
from_search: PostStore.getSearchTerm()
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null,
|
||||
is_mention_search: self.props.isMentionSearch
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
dispatchError(err, "getPost");
|
||||
}
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch});
|
||||
var channelName = "";
|
||||
var channel = ChannelStore.get(this.props.post.channel_id)
|
||||
|
||||
if (channel) {
|
||||
if (channel.type === 'D') {
|
||||
channelName = "Direct Message";
|
||||
} else {
|
||||
channelName = channel.display_name;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-item-container post" onClick={this.handleClick}>
|
||||
<div className="search-channel__name">{ channelName }</div>
|
||||
<div className="post-profile-img__container">
|
||||
<img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" />
|
||||
</div>
|
||||
<div className="post__content">
|
||||
<ul className="post-header">
|
||||
<li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
|
||||
<li className="post-header-col"><time className="search-item-time">{ utils.displayDate(this.props.post.create_at)+' '+utils.displayTime(this.props.post.create_at) }</time></li>
|
||||
</ul>
|
||||
<div className="search-item-snippet"><span>{message}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getStateFromStores() {
|
||||
return { results: PostStore.getSearchResults() };
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
PostStore.addSearchChangeListener(this._onChange);
|
||||
this.resize();
|
||||
var self = this;
|
||||
$(window).resize(function(){
|
||||
self.resize();
|
||||
});
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.resize();
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeSearchChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
if (this.isMounted()) {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
resize: function() {
|
||||
var height = $(window).height() - $('#error_bar').outerHeight() - 100;
|
||||
$("#search-items-container").css("height", height + "px");
|
||||
$("#search-items-container").scrollTop(0);
|
||||
$("#search-items-container").perfectScrollbar();
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var results = this.state.results;
|
||||
var currentId = UserStore.getCurrentId();
|
||||
var searchForm = currentId == null ? null : <SearchBox />;
|
||||
|
||||
if (results == null) {
|
||||
return (
|
||||
<div className="sidebar--right__header">
|
||||
<div className="sidebar__heading">Search Results</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!results.order || results.order.length == 0) {
|
||||
return (
|
||||
<div className="sidebar--right__content">
|
||||
<div className="search-bar__container">{searchForm}</div>
|
||||
<div className="sidebar-right__body">
|
||||
<RhsHeaderSearch />
|
||||
<div id="search-items-container" className="search-items-container">
|
||||
<div className="sidebar--right__subheader">No results</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="sidebar--right__content">
|
||||
<div className="search-bar__container sidebar--right__search-header">{searchForm}</div>
|
||||
<div className="sidebar-right__body">
|
||||
<RhsHeaderSearch isMentionSearch={this.props.isMentionSearch} />
|
||||
<div id="search-items-container" className="search-items-container">
|
||||
{results.order.map(function(id) {
|
||||
var post = results.posts[id];
|
||||
return <SearchItem key={post.id} post={post} term={PostStore.getSearchTerm()} isMentionSearch={self.props.isMentionSearch} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
31
web/react/components/setting_item_max.jsx
Обычный файл
31
web/react/components/setting_item_max.jsx
Обычный файл
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
var client_error = this.props.client_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.client_error }</label></div> : null;
|
||||
var server_error = this.props.server_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.server_error }</label></div> : null;
|
||||
|
||||
var inputs = this.props.inputs;
|
||||
|
||||
return (
|
||||
<ul className="section-max form-horizontal">
|
||||
<li className="col-sm-12 section-title">{this.props.title}</li>
|
||||
<li className="col-sm-9 col-sm-offset-3">
|
||||
<ul className="setting-list">
|
||||
<li className="row setting-list-item form-group">
|
||||
{inputs}
|
||||
</li>
|
||||
<li className="setting-list-item">
|
||||
<hr />
|
||||
{ server_error }
|
||||
{ client_error }
|
||||
<a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a>
|
||||
<a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
14
web/react/components/setting_item_min.jsx
Обычный файл
14
web/react/components/setting_item_min.jsx
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<ul className="section-min">
|
||||
<li className="col-sm-10 section-title">{this.props.title}</li>
|
||||
<li className="col-sm-2 section-edit"><a className="section-edit theme" href="#" onClick={this.props.updateSection}>Edit</a></li>
|
||||
<li className="col-sm-7 section-describe">{this.props.describe}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
55
web/react/components/setting_picture.jsx
Обычный файл
55
web/react/components/setting_picture.jsx
Обычный файл
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
setPicture: function(file) {
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
|
||||
var img = this.refs.image.getDOMNode();
|
||||
reader.onload = function (e) {
|
||||
$(img).attr('src', e.target.result)
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.picture) {
|
||||
this.setPicture(nextProps.picture);
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var client_error = this.props.client_error ? <div className='form-group has-error'><label className='control-label'>{ this.props.client_error }</label></div> : null;
|
||||
var server_error = this.props.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.props.server_error }</label></div> : null;
|
||||
|
||||
var img = null;
|
||||
if (this.props.picture) {
|
||||
img = (<img ref="image" className="col-xs-5 profile-img" src=""/>);
|
||||
} else {
|
||||
img = (<img ref="image" className="col-xs-5 profile-img" src={this.props.src}/>);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
<ul className="section-max">
|
||||
<li className="col-xs-12 section-title">{this.props.title}</li>
|
||||
<li className="col-xs-offset-3 col-xs-8">
|
||||
<ul className="setting-list">
|
||||
<li className="row setting-list-item">
|
||||
{img}
|
||||
</li>
|
||||
<li className="setting-list-item">
|
||||
{ server_error }
|
||||
{ client_error }
|
||||
<span className="btn btn-sm btn-primary btn-file sel-btn">Upload<input ref="input" accept=".jpg,.png,.bmp" type="file" onChange={this.props.pictureChange}/></span>
|
||||
<a className={this.props.submitActive ? "btn btn-sm btn-primary" : "btn btn-sm btn-inactive disabled"} onClick={this.props.submit}>Save</a>
|
||||
<a className="btn btn-sm theme" href="#" onClick={self.props.updateSection}>Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
59
web/react/components/settings_modal.jsx
Обычный файл
59
web/react/components/settings_modal.jsx
Обычный файл
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SettingsSidebar = require('./settings_sidebar.jsx');
|
||||
var UserSettings = require('./user_settings.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
$('body').on('click', '.modal-back', function(){
|
||||
$(this).closest('.modal-dialog').removeClass('display--content');
|
||||
});
|
||||
$('body').on('click', '.modal-header .close', function(){
|
||||
setTimeout(function() {
|
||||
$('.modal-dialog.display--content').removeClass('display--content');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
updateTab: function(tab) {
|
||||
this.setState({ active_tab: tab });
|
||||
},
|
||||
updateSection: function(section) {
|
||||
this.setState({ active_section: section });
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { active_tab: "general", active_section: "" };
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="settings_modal" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog settings-modal">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 className="modal-title" ref="title">Account Settings</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="settings-table">
|
||||
<div className="settings-links">
|
||||
<SettingsSidebar
|
||||
activeTab={this.state.active_tab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-content">
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
updateSection={this.updateSection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
24
web/react/components/settings_sidebar.jsx
Обычный файл
24
web/react/components/settings_sidebar.jsx
Обычный файл
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
module.exports = React.createClass({
|
||||
updateTab: function(tab) {
|
||||
this.props.updateTab(tab);
|
||||
$('.settings-modal').addClass('display--content');
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
return (
|
||||
<div className="">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li className={this.props.activeTab == 'general' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("general");}}><i className="glyphicon glyphicon-cog"></i>General</a></li>
|
||||
<li className={this.props.activeTab == 'security' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("security");}}><i className="glyphicon glyphicon-lock"></i>Security</a></li>
|
||||
<li className={this.props.activeTab == 'notifications' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("notifications");}}><i className="glyphicon glyphicon-exclamation-sign"></i>Notifications</a></li>
|
||||
<li className={this.props.activeTab == 'sessions' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("sessions");}}><i className="glyphicon glyphicon-globe"></i>Sessions</a></li>
|
||||
<li className={this.props.activeTab == 'activity_log' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("activity_log");}}><i className="glyphicon glyphicon-time"></i>Activity Log</a></li>
|
||||
<li className={this.props.activeTab == 'appearance' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("appearance");}}><i className="glyphicon glyphicon-wrench"></i>Appearance</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
449
web/react/components/sidebar.jsx
Обычный файл
449
web/react/components/sidebar.jsx
Обычный файл
@@ -0,0 +1,449 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var SidebarHeader = require('./sidebar_header.jsx');
|
||||
var SearchBox = require('./search_bar.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var SidebarLoginForm = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var state = { }
|
||||
|
||||
var domain = this.refs.domain.getDOMNode().value.trim();
|
||||
if (!domain) {
|
||||
state.server_error = "A domain is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim();
|
||||
if (!email) {
|
||||
state.server_error = "An email is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
var password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!password) {
|
||||
state.server_error = "A password is required"
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.server_error = "";
|
||||
this.setState(state);
|
||||
|
||||
client.loginByEmail(domain, email, password,
|
||||
function(data) {
|
||||
UserStore.setLastDomain(domain);
|
||||
UserStore.setLastEmail(email);
|
||||
UserStore.setCurrentUser(data);
|
||||
|
||||
var redirect = utils.getUrlParameter("redirect");
|
||||
if (redirect) {
|
||||
window.location.href = decodeURI(redirect);
|
||||
} else {
|
||||
window.location.href = '/channels/town-square';
|
||||
}
|
||||
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
if (err.message == "Login failed because email address has not been verified") {
|
||||
window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email);
|
||||
return;
|
||||
}
|
||||
state.server_error = err.message;
|
||||
this.valid = false;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null;
|
||||
|
||||
var subDomain = utils.getSubDomain();
|
||||
var subDomainClass = "form-control hidden";
|
||||
|
||||
if (subDomain == "") {
|
||||
subDomain = UserStore.getLastDomain();
|
||||
subDomainClass = "form-control";
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="" onSubmit={this.handleSubmit}>
|
||||
<a href="/find_team">{"Find your " + strings.Team}</a>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
{ server_error }
|
||||
<input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="text" className="form-control" name="email" defaultValue={UserStore.getLastEmail()} ref="email" placeholder="Email" />
|
||||
</div>
|
||||
<div className={server_error ? 'form-group has-error' : 'form-group'}>
|
||||
<input type="password" className="form-control" name="password" ref="password" placeholder="Password" />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-default">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getStateFromStores() {
|
||||
var members = ChannelStore.getAllMembers();
|
||||
var team_member_map = UserStore.getActiveOnlyProfiles();
|
||||
var current_id = ChannelStore.getCurrentId();
|
||||
|
||||
var teammates = [];
|
||||
for (var id in team_member_map) {
|
||||
if (id === UserStore.getCurrentId()) continue;
|
||||
teammates.push(team_member_map[id]);
|
||||
}
|
||||
|
||||
// Create lists of all read and unread direct channels
|
||||
var showDirectChannels = [];
|
||||
var readDirectChannels = [];
|
||||
for (var i = 0; i < teammates.length; i++) {
|
||||
var teammate = teammates[i];
|
||||
|
||||
if (teammate.id == UserStore.getCurrentId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var channelName = "";
|
||||
if (teammate.id > UserStore.getCurrentId()) {
|
||||
channelName = UserStore.getCurrentId() + '__' + teammate.id;
|
||||
} else {
|
||||
channelName = teammate.id + '__' + UserStore.getCurrentId();
|
||||
}
|
||||
|
||||
var channel = ChannelStore.getByName(channelName);
|
||||
|
||||
if (channel != null) {
|
||||
channel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username;
|
||||
channel.teammate_username = teammate.username;
|
||||
|
||||
channel.status = UserStore.getStatus(teammate.id);
|
||||
|
||||
var channelMember = members[channel.id];
|
||||
var msg_count = channel.total_msg_count - channelMember.msg_count;
|
||||
if (msg_count > 0) {
|
||||
channel.unread = msg_count;
|
||||
showDirectChannels.push(channel);
|
||||
} else if (current_id === channel.id) {
|
||||
showDirectChannels.push(channel);
|
||||
} else {
|
||||
readDirectChannels.push(channel);
|
||||
}
|
||||
} else {
|
||||
var tempChannel = {};
|
||||
tempChannel.fake = true;
|
||||
tempChannel.name = channelName;
|
||||
tempChannel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username;
|
||||
tempChannel.status = UserStore.getStatus(teammate.id);
|
||||
tempChannel.last_post_at = 0;
|
||||
readDirectChannels.push(tempChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have MAX_DMS unread channels, sort the read list by last_post_at
|
||||
if (showDirectChannels.length < Constants.MAX_DMS) {
|
||||
readDirectChannels.sort(function(a,b) {
|
||||
// sort by last_post_at first
|
||||
if (a.last_post_at > b.last_post_at) return -1;
|
||||
if (a.last_post_at < b.last_post_at) return 1;
|
||||
// if last_post_at is equal, sort by name
|
||||
if (a.display_name < b.display_name) return -1;
|
||||
if (a.display_name > b.display_name) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var index = 0;
|
||||
while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) {
|
||||
showDirectChannels.push(readDirectChannels[index]);
|
||||
index++;
|
||||
}
|
||||
readDirectChannels = readDirectChannels.slice(index);
|
||||
|
||||
showDirectChannels.sort(function(a,b) {
|
||||
if (a.display_name < b.display_name) return -1;
|
||||
if (a.display_name > b.display_name) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
active_id: current_id,
|
||||
channels: ChannelStore.getAll(),
|
||||
members: members,
|
||||
showDirectChannels: showDirectChannels,
|
||||
hideDirectChannels: readDirectChannels
|
||||
};
|
||||
}
|
||||
|
||||
var SidebarLoggedIn = React.createClass({
|
||||
componentDidMount: function() {
|
||||
ChannelStore.addChangeListener(this._onChange);
|
||||
UserStore.addChangeListener(this._onChange);
|
||||
UserStore.addStatusesChangeListener(this._onChange);
|
||||
SocketStore.addChangeListener(this._onSocketChange);
|
||||
$(".nav-pills__container").perfectScrollbar();
|
||||
|
||||
this.updateTitle();
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.updateTitle();
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ChannelStore.removeChangeListener(this._onChange);
|
||||
UserStore.removeChangeListener(this._onChange);
|
||||
UserStore.removeStatusesChangeListener(this._onChange);
|
||||
SocketStore.removeChangeListener(this._onSocketChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
_onSocketChange: function(msg) {
|
||||
if (msg.action == "posted") {
|
||||
if (ChannelStore.getCurrentId() === msg.channel_id) {
|
||||
AsyncClient.getChannels(true, window.isActive);
|
||||
} else {
|
||||
AsyncClient.getChannels(true);
|
||||
}
|
||||
|
||||
if (UserStore.getCurrentId() != msg.user_id) {
|
||||
|
||||
var mentions = msg.props.mentions ? JSON.parse(msg.props.mentions) : [];
|
||||
var channel = ChannelStore.get(msg.channel_id);
|
||||
|
||||
var user = UserStore.getCurrentUser();
|
||||
if (user.notify_props && ((user.notify_props.desktop === "mention" && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === "none")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var member = ChannelStore.getMember(msg.channel_id);
|
||||
if ((member.notify_level === "mention" && mentions.indexOf(user.id) === -1) || member.notify_level === "none" || member.notify_level === "quiet") {
|
||||
return;
|
||||
}
|
||||
|
||||
var username = "Someone";
|
||||
if (UserStore.hasProfile(msg.user_id)) {
|
||||
username = UserStore.getProfile(msg.user_id).username;
|
||||
}
|
||||
|
||||
var title = channel ? channel.display_name : "Posted";
|
||||
|
||||
var repRegex = new RegExp("<br>", "g");
|
||||
var post = JSON.parse(msg.props.post);
|
||||
var msg = post.message.replace(repRegex, "\n").split("\n")[0].replace("<mention>", "").replace("</mention>", "");
|
||||
if (msg.length > 50) {
|
||||
msg = msg.substring(0,49) + "...";
|
||||
}
|
||||
utils.notifyMe(title, username + " wrote: " + msg, channel);
|
||||
if (!user.notify_props || user.notify_props.desktop_sound === "true") {
|
||||
utils.ding();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg.action == "viewed") {
|
||||
if (ChannelStore.getCurrentId() != msg.channel_id) {
|
||||
AsyncClient.getChannels(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTitle: function() {
|
||||
var channel = ChannelStore.getCurrent();
|
||||
if (channel) {
|
||||
if (channel.type === 'D') {
|
||||
userIds = channel.name.split('__');
|
||||
if (userIds.length < 2) return;
|
||||
if (userIds[0] == UserStore.getCurrentId() && UserStore.getProfile(userIds[1])) {
|
||||
document.title = UserStore.getProfile(userIds[1]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
} else if (userIds[1] == UserStore.getCurrentId() && UserStore.getProfile(userIds[0])) {
|
||||
document.title = UserStore.getProfile(userIds[0]).username + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
}
|
||||
} else {
|
||||
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"))
|
||||
}
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var members = this.state.members;
|
||||
var newsActive = window.location.pathname === "/" ? "active" : "";
|
||||
var badgesActive = false;
|
||||
var self = this;
|
||||
var channelItems = this.state.channels.map(function(channel) {
|
||||
if (channel.type != 'O') {
|
||||
return "";
|
||||
}
|
||||
|
||||
var channelMember = members[channel.id];
|
||||
var active = channel.id === self.state.active_id ? "active" : "";
|
||||
|
||||
var msg_count = channel.total_msg_count - channelMember.msg_count;
|
||||
var titleClass = ""
|
||||
if (msg_count > 0 && channelMember.notify_level !== "quiet") {
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
|
||||
var badge = "";
|
||||
if (channelMember.mention_count > 0) {
|
||||
badge = <span className="badge pull-right small">{channelMember.mention_count}</span>;
|
||||
badgesActive = true;
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={channel.id} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li>
|
||||
);
|
||||
});
|
||||
|
||||
var privateChannelItems = this.state.channels.map(function(channel) {
|
||||
if (channel.type != 'P') {
|
||||
return "";
|
||||
}
|
||||
|
||||
var channelMember = members[channel.id];
|
||||
var active = channel.id === self.state.active_id ? "active" : "";
|
||||
|
||||
var msg_count = channel.total_msg_count - channelMember.msg_count;
|
||||
var titleClass = ""
|
||||
if (msg_count > 0 && channelMember.notify_level !== "quiet") {
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
|
||||
var badge = "";
|
||||
if (channelMember.mention_count > 0) {
|
||||
badge = <span className="badge pull-right small">{channelMember.mention_count}</span>;
|
||||
badgesActive = true;
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={channel.id} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li>
|
||||
);
|
||||
});
|
||||
|
||||
var directMessageItems = this.state.showDirectChannels.map(function(channel) {
|
||||
var badge = "";
|
||||
var titleClass = "";
|
||||
|
||||
var statusIcon = "";
|
||||
if (channel.status === "online") {
|
||||
statusIcon = Constants.ONLINE_ICON_SVG;
|
||||
} else if (channel.status === "away") {
|
||||
statusIcon = Constants.ONLINE_ICON_SVG;
|
||||
} else {
|
||||
statusIcon = Constants.OFFLINE_ICON_SVG;
|
||||
}
|
||||
|
||||
if (!channel.fake) {
|
||||
var active = channel.id === self.state.active_id ? "active" : "";
|
||||
|
||||
if (channel.unread) {
|
||||
badge = <span className="badge pull-right small">{channel.unread}</span>;
|
||||
badgesActive = true;
|
||||
titleClass = "unread-title"
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel, channel.teammate_username);}}><span className="status" dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href={"/channels/"+channel.name}><span className="status" dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
link.id = 'favicon';
|
||||
if (badgesActive) {
|
||||
link.href = '/static/images/redfavicon.ico';
|
||||
} else {
|
||||
link.href = '/static/images/favicon.ico';
|
||||
}
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var oldLink = document.getElementById('favicon');
|
||||
if (oldLink) {
|
||||
head.removeChild(oldLink);
|
||||
}
|
||||
head.appendChild(link);
|
||||
|
||||
if (channelItems.length == 0) {
|
||||
<li><small>Loading...</small></li>
|
||||
}
|
||||
|
||||
if (privateChannelItems.length == 0) {
|
||||
<li><small>Loading...</small></li>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<SidebarHeader teamName={this.props.teamName} teamType={this.props.teamType} />
|
||||
<SearchBox />
|
||||
|
||||
<div className="nav-pills__container">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><h4>Channels<a className="add-channel-btn" href="#" data-toggle="modal" data-target="#new_channel" data-channeltype="O">+</a></h4></li>
|
||||
{channelItems}
|
||||
<li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_channels" data-channeltype="O">More...</a></li>
|
||||
</ul>
|
||||
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><h4>Private Groups<a className="add-channel-btn" href="#" data-toggle="modal" data-target="#new_channel" data-channeltype="P">+</a></h4></li>
|
||||
{privateChannelItems}
|
||||
</ul>
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><h4>Direct Messages</h4></li>
|
||||
{directMessageItems}
|
||||
{ this.state.hideDirectChannels.length > 0 ?
|
||||
<li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_direct_channels" data-channels={JSON.stringify(this.state.hideDirectChannels)}>{"More ("+this.state.hideDirectChannels.length+")"}</a></li>
|
||||
: "" }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SidebarLoggedOut = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<SidebarHeader teamName={this.props.teamName} />
|
||||
<SidebarLoginForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
render: function() {
|
||||
var currentId = UserStore.getCurrentId();
|
||||
if (currentId != null) {
|
||||
return <SidebarLoggedIn teamName={this.props.teamName} teamType={this.props.teamType} />;
|
||||
} else {
|
||||
return <SidebarLoggedOut teamName={this.props.teamName} />;
|
||||
}
|
||||
}
|
||||
});
|
||||
134
web/react/components/sidebar_header.jsx
Обычный файл
134
web/react/components/sidebar_header.jsx
Обычный файл
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return { teams: UserStore.getTeams() };
|
||||
}
|
||||
|
||||
var NavbarDropdown = React.createClass({
|
||||
handleLogoutClick: function(e) {
|
||||
e.preventDefault();
|
||||
client.logout();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
UserStore.addTeamsChangeListener(this._onChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
UserStore.removeTeamsChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
if (this.isMounted()) {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var team_link = "";
|
||||
var invite_link = "";
|
||||
var manage_link = "";
|
||||
var rename_link = "";
|
||||
var currentUser = UserStore.getCurrentUser()
|
||||
var isAdmin = false;
|
||||
|
||||
if (currentUser != null) {
|
||||
isAdmin = currentUser.roles.indexOf("admin") > -1;
|
||||
|
||||
invite_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (this.props.teamType == "O") {
|
||||
team_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#get_link" data-title="Team Invite" data-value={location.origin+"/signup_user_complete/?id="+currentUser.team_id}>Get Team Invite Link</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
manage_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#team_members">Manage Team</a>
|
||||
</li>
|
||||
);
|
||||
rename_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#rename_team_link">Rename</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var teams = [];
|
||||
|
||||
if (this.state.teams.length > 1) {
|
||||
for (var i = 0; i < this.state.teams.length; i++) {
|
||||
var domain = this.state.teams[i];
|
||||
|
||||
if (domain == utils.getSubDomain())
|
||||
continue;
|
||||
|
||||
if (teams.length == 0)
|
||||
teams.push(<li className="divider" key="div"></li>);
|
||||
|
||||
teams.push(<li key={ domain }><a href={window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub() }>Switch to { domain }</a></li>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="nav navbar-nav navbar-right">
|
||||
<li className="dropdown">
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
||||
<i className="dropdown__icon"></i>
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-toggle="modal" data-target="#settings_modal">Account Settings</a></li>
|
||||
{ invite_link }
|
||||
{ team_link }
|
||||
{ manage_link }
|
||||
{ rename_link }
|
||||
<li><a href="#" onClick={this.handleLogoutClick}>Logout</a></li>
|
||||
{ teams }
|
||||
<li className="divider"></li>
|
||||
<li><a target="_blank" href={config.HelpLink}>Help</a></li>
|
||||
<li><a target="_blank" href={config.ReportProblemLink}>Report a Problem</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
var teamName = this.props.teamName ? this.props.teamName : config.SiteName;
|
||||
|
||||
return (
|
||||
<div className="team__header theme">
|
||||
<a className="team__name" href="/channels/town-square">{ teamName }</a>
|
||||
<NavbarDropdown teamType={this.props.teamType} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
84
web/react/components/sidebar_right.jsx
Обычный файл
84
web/react/components/sidebar_right.jsx
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var SearchResults =require('./search_results.jsx');
|
||||
var PostRight =require('./post_right.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
function getStateFromStores(from_search) {
|
||||
return { search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch() };
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
PostStore.addSearchChangeListener(this._onSearchChange);
|
||||
PostStore.addSelectedPostChangeListener(this._onSelectedChange);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeSearchChangeListener(this._onSearchChange);
|
||||
PostStore.removeSelectedPostChangeListener(this._onSelectedChange);
|
||||
},
|
||||
_onSelectedChange: function(from_search) {
|
||||
if (this.isMounted()) {
|
||||
var newState = getStateFromStores(from_search);
|
||||
newState.from_search = from_search;
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
_onSearchChange: function() {
|
||||
if (this.isMounted()) {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
resize: function() {
|
||||
$(".post-list-holder-by-time").scrollTop(100000);
|
||||
$(".post-list-holder-by-time").perfectScrollbar('update');
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
if (! (this.state.search_visible || this.state.post_right_visible)) {
|
||||
$('.inner__wrap').removeClass('move--left').removeClass('move--right');
|
||||
$('.sidebar--right').removeClass('move--left');
|
||||
this.resize();
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
$('.inner__wrap').removeClass('.move--right').addClass('move--left');
|
||||
$('.sidebar--left').removeClass('move--right');
|
||||
$('.sidebar--right').addClass('move--left');
|
||||
$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
|
||||
this.resize();
|
||||
setTimeout(function(){
|
||||
$('.sidebar__overlay').fadeOut("200", function(){
|
||||
$(this).remove();
|
||||
});
|
||||
},500)
|
||||
|
||||
var content = "";
|
||||
|
||||
if (this.state.search_visible) {
|
||||
content = <SearchResults isMentionSearch={this.state.is_mention_search} />;
|
||||
}
|
||||
else if (this.state.post_right_visible) {
|
||||
content = <PostRight fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sidebar-right-container">
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
76
web/react/components/sidebar_right_menu.jsx
Обычный файл
76
web/react/components/sidebar_right_menu.jsx
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleLogoutClick: function(e) {
|
||||
e.preventDefault();
|
||||
client.logout();
|
||||
},
|
||||
render: function() {
|
||||
var team_link = "";
|
||||
var invite_link = "";
|
||||
var manage_link = "";
|
||||
var rename_link = "";
|
||||
var currentUser = UserStore.getCurrentUser()
|
||||
var isAdmin = false;
|
||||
|
||||
if (currentUser != null) {
|
||||
isAdmin = currentUser.roles.indexOf("admin") > -1;
|
||||
|
||||
invite_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#invite_member"><i className="glyphicon glyphicon-user"></i>Invite New Member</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (this.props.teamType == "O") {
|
||||
team_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#get_link" data-title="Team Invite" data-value={location.origin+"/signup_user_complete/?id="+currentUser.team_id}><i className="glyphicon glyphicon-link"></i>Get Team Invite Link</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
manage_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#team_members"><i className="glyphicon glyphicon-wrench"></i>Manage Team</a>
|
||||
</li>
|
||||
);
|
||||
rename_link = (
|
||||
<li>
|
||||
<a href="#" data-toggle="modal" data-target="#rename_team_link"><i className="glyphicon glyphicon-pencil"></i>Rename</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var siteName = config.SiteName != null ? config.SiteName : "";
|
||||
var teamName = this.props.teamName ? this.props.teamName : siteName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="team__header theme">
|
||||
<a className="team__name" href="/channels/town-square">{ teamName }</a>
|
||||
</div>
|
||||
|
||||
<div className="nav-pills__container">
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
<li><a href="#" data-toggle="modal" data-target="#settings_modal"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li>
|
||||
{ invite_link }
|
||||
{ team_link }
|
||||
{ manage_link }
|
||||
{ rename_link }
|
||||
<li><a href="#" onClick={this.handleLogoutClick}><i className="glyphicon glyphicon-log-out"></i>Logout</a></li>
|
||||
<li className="divider"></li>
|
||||
<li><a target="_blank" href="/static/help/configure_links.html"><i className="glyphicon glyphicon-question-sign"></i>Help</a></li>
|
||||
<li><a target="_blank" href="/static/help/configure_links.html"><i className="glyphicon glyphicon-earphone"></i>Report a Problem</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
78
web/react/components/signup_team.jsx
Обычный файл
78
web/react/components/signup_team.jsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var team = {};
|
||||
var state = { server_error: "" };
|
||||
|
||||
team.email = this.refs.email.getDOMNode().value.trim().toLowerCase();
|
||||
if (!team.email || !utils.isEmail(team.email)) {
|
||||
state.email_error = "Please enter a valid email address";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.email_error = "";
|
||||
}
|
||||
|
||||
team.name = this.refs.name.getDOMNode().value.trim();
|
||||
if (!team.name) {
|
||||
state.name_error = "This field is required";
|
||||
state.inValid = true;
|
||||
}
|
||||
else {
|
||||
state.name_error = "";
|
||||
}
|
||||
|
||||
if (state.inValid) {
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
client.signupTeam(team.email, team.name,
|
||||
function(data) {
|
||||
if (data["follow_link"]) {
|
||||
window.location.href = data["follow_link"];
|
||||
}
|
||||
else {
|
||||
window.location.href = "/signup_team_confirm/?email=" + encodeURIComponent(team.email);
|
||||
}
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
state.server_error = err.message;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null;
|
||||
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<form role="form" onSubmit={this.handleSubmit}>
|
||||
<div className={ email_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" />
|
||||
{ email_error }
|
||||
</div>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="text" ref="name" className="form-control" placeholder={utils.toTitleCase(strings.Company) + " Name"} maxLength="64" />
|
||||
{ name_error }
|
||||
</div>
|
||||
{ server_error }
|
||||
<button className="btn btn-md btn-primary" type="submit">Sign up for Free</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
644
web/react/components/signup_team_complete.jsx
Обычный файл
644
web/react/components/signup_team_complete.jsx
Обычный файл
@@ -0,0 +1,644 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var constants = require('../utils/constants.jsx')
|
||||
|
||||
WelcomePage = React.createClass({
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "team_name";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
handleDiffEmail: function (e) {
|
||||
e.preventDefault();
|
||||
this.setState({ use_diff: true });
|
||||
},
|
||||
handleDiffSubmit: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var state = { use_diff: true, server_error: "" };
|
||||
|
||||
var email = this.refs.email.getDOMNode().value.trim().toLowerCase();
|
||||
if (!email || !utils.isEmail(email)) {
|
||||
state.email_error = "Please enter a valid email address";
|
||||
this.setState(state);
|
||||
}
|
||||
else {
|
||||
state.email_error = "";
|
||||
}
|
||||
|
||||
client.signupTeam(email, this.props.state.team.name,
|
||||
function(data) {
|
||||
this.props.state.wizard = "finished";
|
||||
this.props.updateParent(this.props.state);
|
||||
window.location.href = "/signup_team_confirm/?email=" + encodeURI(email);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.server_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { use_diff: false };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_01_welcome');
|
||||
|
||||
var email_error = this.state.email_error ? <label className="control-label">{ this.state.email_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className="control-label">{ this.state.server_error }</label></div> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>Welcome!</h2>
|
||||
<h3>{"Let's set up your " + strings.Team + " on " + config.SiteName + "."}</h3>
|
||||
</p>
|
||||
<p>
|
||||
Please confirm your email address:<br />
|
||||
<span className="black">{ this.props.state.team.email }</span><br />
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<button className="btn-primary btn form-group" onClick={this.submitNext}><i className="glyphicon glyphicon-ok"></i>Yes, this address is correct</button>
|
||||
</div>
|
||||
<hr />
|
||||
<p>If this is not correct, you can switch to a different email. We'll send you a new invite right away.</p>
|
||||
<div className={ this.state.use_diff ? "" : "hidden" }>
|
||||
<div className={ email_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<input type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" />
|
||||
</div>
|
||||
</div>
|
||||
{ email_error }
|
||||
</div>
|
||||
{ server_error }
|
||||
<button className="btn btn-md btn-primary" onClick={this.handleDiffSubmit} type="submit">Use this instead</button>
|
||||
</div>
|
||||
<button onClick={this.handleDiffEmail} className={ this.state.use_diff ? "btn-default btn hidden" : "btn-default btn" }>Use a different address</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
TeamNamePage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "welcome";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var name = this.refs.name.getDOMNode().value.trim();
|
||||
if (!name) {
|
||||
this.setState({name_error: "This field is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.state.wizard = "team_url";
|
||||
this.props.state.team.name = name;
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_02_name');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
|
||||
<h2>{utils.toTitleCase(strings.Team) + " Name"}</h2>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} />
|
||||
</div>
|
||||
</div>
|
||||
{ name_error }
|
||||
</div>
|
||||
<p>{"Your " + strings.Team + " name shows in menus and headings. It may include the name of your " + strings.Company + ", but it's not required."}</p>
|
||||
<button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button>
|
||||
<button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
TeamUrlPage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "team_name";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var name = this.refs.name.getDOMNode().value.trim();
|
||||
if (!name) {
|
||||
this.setState({name_error: "This field is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
var cleaned_name = utils.cleanUpUrlable(name);
|
||||
if (cleaned_name != name) {
|
||||
this.setState({name_error: "Must be lowercase alphanumeric characters"});
|
||||
return;
|
||||
}
|
||||
else if (cleaned_name.length <= 3 || cleaned_name.length > 15) {
|
||||
this.setState({name_error: "Domain must be 4 or more characters up to a maximum of 15"})
|
||||
return;
|
||||
}
|
||||
|
||||
for (var index = 0; index < constants.RESERVED_DOMAINS.length; index++) {
|
||||
if (cleaned_name.indexOf(constants.RESERVED_DOMAINS[index]) == 0) {
|
||||
this.setState({name_error: "This Team URL name is unavailable"})
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client.findTeamByDomain(name,
|
||||
function(data) {
|
||||
if (!data) {
|
||||
if (config.AllowSignupDomainsWizard) {
|
||||
this.props.state.wizard = "allowed_domains";
|
||||
} else {
|
||||
this.props.state.wizard = "send_invites";
|
||||
this.props.state.team.type = 'O';
|
||||
}
|
||||
|
||||
this.props.state.team.domain = name;
|
||||
this.props.updateParent(this.props.state);
|
||||
}
|
||||
else {
|
||||
this.state.name_error = "This URL is unavailable. Please try another.";
|
||||
this.setState(this.state);
|
||||
}
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.name_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_03_url');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>{utils.toTitleCase(strings.Team) + " URL"}</h2>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<div className="input-group">
|
||||
<input type="text" ref="name" className="form-control text-right" placeholder="" maxLength="128" defaultValue={this.props.state.team.domain} />
|
||||
<span className="input-group-addon">.{ utils.getDomainWithOutSub() }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ name_error }
|
||||
</div>
|
||||
<p className="black">{"Pick something short and memorable for your " + strings.Team + "'s web address."}</p>
|
||||
<p>{"Your " + strings.Team + " URL can only contain lowercase letters, numbers and dashes. Also, it needs to start with a letter and cannot end in a dash."}</p>
|
||||
<button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button>
|
||||
<button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
AllowedDomainsPage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "team_url";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.refs.open_network.getDOMNode().checked) {
|
||||
this.props.state.wizard = "send_invites";
|
||||
this.props.state.team.type = 'O';
|
||||
this.props.updateParent(this.props.state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.refs.allow.getDOMNode().checked) {
|
||||
var name = this.refs.name.getDOMNode().value.trim();
|
||||
var domainRegex = /^\w+\.\w+$/
|
||||
if (!name) {
|
||||
this.setState({name_error: "This field is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!name.trim().match(domainRegex)) {
|
||||
this.setState({name_error: "The domain doesn't appear valid"});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.state.wizard = "send_invites";
|
||||
this.props.state.team.allowed_domains = name;
|
||||
this.props.state.team.type = 'I';
|
||||
this.props.updateParent(this.props.state);
|
||||
}
|
||||
else {
|
||||
this.props.state.wizard = "send_invites";
|
||||
this.props.state.team.type = 'I';
|
||||
this.props.updateParent(this.props.state);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_04_allow_domains');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>Email Domain</h2>
|
||||
<p>
|
||||
<div className="checkbox"><label><input type="checkbox" ref="allow" defaultChecked />{" Allow sign up and " + strings.Team + " discovery with a " + strings.Company + " email address."}</label></div>
|
||||
</p>
|
||||
<p>{"Check this box to allow your " + strings.Team + " members to sign up using their " + strings.Company + " email addresses if you share the same domain--otherwise, you need to invite everyone yourself."}</p>
|
||||
<h4>{"Your " + strings.Team + "'s domain for emails"}</h4>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<div className="input-group">
|
||||
<span className="input-group-addon">@</span>
|
||||
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.allowed_domains} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ name_error }
|
||||
</div>
|
||||
<p>To allow signups from multiple domains, separate each with a comma.</p>
|
||||
<p>
|
||||
<div className="checkbox"><label><input type="checkbox" ref="open_network" defaultChecked={this.props.state.team.type == 'O'} /> Allow anyone to signup to this domain without an invitation.</label></div>
|
||||
</p>
|
||||
<button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button>
|
||||
<button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
EmailItem = React.createClass({
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
getValue: function() {
|
||||
return this.refs.email.getDOMNode().value.trim()
|
||||
},
|
||||
validate: function() {
|
||||
var email = this.refs.email.getDOMNode().value.trim().toLowerCase();
|
||||
|
||||
if (!email) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!utils.isEmail(email)) {
|
||||
this.state.email_error = "Please enter a valid email address";
|
||||
this.setState(this.state);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
this.state.email_error = "";
|
||||
this.setState(this.state);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var email_error = this.state.email_error ? <label className="control-label">{ this.state.email_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div className={ email_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="email" ref="email" className="form-control" placeholder="Email Address" defaultValue={this.props.email} maxLength="128" />
|
||||
{ email_error }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
SendInivtesPage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (config.AllowSignupDomainsWizard) {
|
||||
this.props.state.wizard = "allowed_domains";
|
||||
} else {
|
||||
this.props.state.wizard = "team_url";
|
||||
}
|
||||
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var valid = true;
|
||||
var emails = [];
|
||||
|
||||
for (var i = 0; i < this.props.state.invites.length; i++) {
|
||||
if (!this.refs['email_' + i].validate()) {
|
||||
valid = false;
|
||||
} else {
|
||||
emails.push(this.refs['email_' + i].getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.state.wizard = "username";
|
||||
this.props.state.invites = emails;
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitAddInvite: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "send_invites";
|
||||
if (this.props.state.invites == null || this.props.state.invites.length == 0) {
|
||||
this.props.state.invites = [];
|
||||
}
|
||||
this.props.state.invites.push("");
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitSkip: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "username";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_05_send_invites');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
var emails = [];
|
||||
|
||||
for (var i = 0; i < this.props.state.invites.length; i++) {
|
||||
emails.push(<EmailItem key={i} ref={'email_' + i} email={this.props.state.invites[i]} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>Send Invitations</h2>
|
||||
{ emails }
|
||||
<div className="form-group"><button className="btn-default btn" onClick={this.submitAddInvite}>Add Invitation</button></div>
|
||||
<div className="form btn-default-group"><button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button></div>
|
||||
<p>{"If you'd prefer, you can send invitations after you finish setting up the "+ strings.Team + "."}</p>
|
||||
<div><a href="#" onClick={this.submitSkip}>Skip this step</a></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
UsernamePage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "send_invites";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var name = this.refs.name.getDOMNode().value.trim();
|
||||
|
||||
var username_error = utils.isValidUsername(name);
|
||||
if (username_error === "Cannot use a reserved word as a username.") {
|
||||
this.setState({name_error: "This username is reserved, please choose a new one." });
|
||||
return;
|
||||
} else if (username_error) {
|
||||
this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.props.state.wizard = "password";
|
||||
this.props.state.user.username = name;
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_06_username');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>Choose a username</h2>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<input type="text" ref="name" className="form-control" placeholder="" defaultValue={this.props.state.user.username} maxLength="128" />
|
||||
</div>
|
||||
</div>
|
||||
{ name_error }
|
||||
</div>
|
||||
<p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}</p>
|
||||
<button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button>
|
||||
<button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
PasswordPage = React.createClass({
|
||||
submitBack: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.state.wizard = "username";
|
||||
this.props.updateParent(this.props.state);
|
||||
},
|
||||
submitNext: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!password || password.length < 5) {
|
||||
this.setState({name_error: "Please enter at least 5 characters"});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#finish-button').button('loading');
|
||||
var teamSignup = JSON.parse(JSON.stringify(this.props.state));
|
||||
teamSignup.user.password = password;
|
||||
teamSignup.user.allow_marketing = this.refs.email_service.getDOMNode().checked;
|
||||
delete teamSignup.wizard;
|
||||
var ctl = this;
|
||||
|
||||
client.createTeamFromSignup(teamSignup,
|
||||
function(data) {
|
||||
|
||||
client.track('signup', 'signup_team_08_complete');
|
||||
|
||||
var props = this.props;
|
||||
|
||||
setTimeout(function() {
|
||||
$('#sign-up-button').button('reset');
|
||||
props.state.wizard = "finished";
|
||||
props.updateParent(props.state, true);
|
||||
|
||||
if (utils.isTestDomain()) {
|
||||
UserStore.setLastDomain(teamSignup.team.domain);
|
||||
UserStore.setLastEmail(teamSignup.team.email);
|
||||
window.location.href = window.location.protocol + '//' + utils.getDomainWithOutSub() + '/login?email=' + encodeURIComponent(teamSignup.team.email);
|
||||
}
|
||||
else {
|
||||
window.location.href = window.location.protocol + '//' + teamSignup.team.domain + '.' + utils.getDomainWithOutSub() + '/login?email=' + encodeURIComponent(teamSignup.team.email);
|
||||
}
|
||||
|
||||
// client.loginByEmail(teamSignup.team.domain, teamSignup.team.email, teamSignup.user.password,
|
||||
// function(data) {
|
||||
// UserStore.setLastDomain(teamSignup.team.domain);
|
||||
// UserStore.setLastEmail(teamSignup.team.email);
|
||||
// UserStore.setCurrentUser(data);
|
||||
// window.location.href = '/channels/town-square';
|
||||
// }.bind(ctl),
|
||||
// function(err) {
|
||||
// this.setState({name_error: err.message});
|
||||
// }.bind(ctl)
|
||||
// );
|
||||
}, 5000);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.setState({name_error: err.message});
|
||||
$('#sign-up-button').button('reset');
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { };
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_team_07_password');
|
||||
|
||||
var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h2>Choose a password</h2>
|
||||
<p>You'll use your email address ({this.props.state.team.email}) and password to log into {config.SiteName}.</p>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<input type="password" ref="password" className="form-control" placeholder="" maxLength="128" />
|
||||
</div>
|
||||
</div>
|
||||
{ name_error }
|
||||
</div>
|
||||
<div className="form-group checkbox">
|
||||
<label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service.</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button>
|
||||
<button className="btn-primary btn" id="finish-button" data-loading-text={"<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Creating "+strings.Team+"..."} onClick={this.submitNext}>Finish</button>
|
||||
</div>
|
||||
<p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
updateParent: function(state, skipSet) {
|
||||
localStorage.setItem(this.props.hash, JSON.stringify(state));
|
||||
|
||||
if (!skipSet) {
|
||||
this.setState(state);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
var props = null;
|
||||
try {
|
||||
props = JSON.parse(localStorage.getItem(this.props.hash));
|
||||
}
|
||||
catch(parse_error) {
|
||||
}
|
||||
|
||||
if (!props) {
|
||||
props = {};
|
||||
props.wizard = "welcome";
|
||||
props.team = {};
|
||||
props.team.email = this.props.email;
|
||||
props.team.name = this.props.name;
|
||||
props.team.company_name = this.props.name;
|
||||
props.team.domain = utils.cleanUpUrlable(this.props.name);
|
||||
props.team.allowed_domains = "";
|
||||
props.invites = [];
|
||||
props.invites.push("");
|
||||
props.invites.push("");
|
||||
props.invites.push("");
|
||||
props.user = {};
|
||||
props.hash = this.props.hash;
|
||||
props.data = this.props.data;
|
||||
}
|
||||
|
||||
return props ;
|
||||
},
|
||||
render: function() {
|
||||
if (this.state.wizard == "welcome") {
|
||||
return <WelcomePage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "team_name") {
|
||||
return <TeamNamePage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "team_url") {
|
||||
return <TeamUrlPage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "allowed_domains") {
|
||||
return <AllowedDomainsPage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "send_invites") {
|
||||
return <SendInivtesPage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "username") {
|
||||
return <UsernamePage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
if (this.state.wizard == "password") {
|
||||
return <PasswordPage state={this.state} updateParent={this.updateParent} />
|
||||
}
|
||||
|
||||
return (<div>You've already completed the signup process for this invitation or this invitation has expired.</div>);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
145
web/react/components/signup_user_complete.jsx
Обычный файл
145
web/react/components/signup_user_complete.jsx
Обычный файл
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.state.user.username = this.refs.name.getDOMNode().value.trim();
|
||||
if (!this.state.user.username) {
|
||||
this.setState({name_error: "This field is required", email_error: "", password_error: ""});
|
||||
return;
|
||||
}
|
||||
|
||||
var username_error = utils.isValidUsername(this.state.user.username)
|
||||
if (username_error === "Cannot use a reserved word as a username.") {
|
||||
this.setState({name_error: "This username is reserved, please choose a new one." });
|
||||
return;
|
||||
} else if (username_error) {
|
||||
this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.user.email = this.refs.email.getDOMNode().value.trim();
|
||||
if (!this.state.user.email) {
|
||||
this.setState({name_error: "", email_error: "This field is required", password_error: ""});
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.user.password = this.refs.password.getDOMNode().value.trim();
|
||||
if (!this.state.user.password || this.state.user.password .length < 5) {
|
||||
this.setState({name_error: "", email_error: "", password_error: "Please enter at least 5 characters"});
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked;
|
||||
|
||||
client.createUser(this.state.user, this.state.data, this.state.hash,
|
||||
function(data) {
|
||||
client.track('signup', 'signup_user_02_complete');
|
||||
|
||||
if (data.email_verified) {
|
||||
client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password,
|
||||
function(data) {
|
||||
UserStore.setLastDomain(this.props.domain);
|
||||
UserStore.setLastEmail(this.state.user.email);
|
||||
UserStore.setCurrentUser(data);
|
||||
if (this.props.hash > 0)
|
||||
localStorage.setItem(this.props.hash, JSON.stringify({wizard: "finished"}));
|
||||
window.location.href = '/channels/town-square';
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.server_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
else {
|
||||
window.location.href = "/verify?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain);
|
||||
}
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
this.state.server_error = err.message;
|
||||
this.setState(this.state);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
var props = null;
|
||||
try {
|
||||
props = JSON.parse(localStorage.getItem(this.props.hash));
|
||||
}
|
||||
catch(parse_error) {
|
||||
}
|
||||
|
||||
if (!props) {
|
||||
props = {};
|
||||
props.wizard = "welcome";
|
||||
props.user = {};
|
||||
props.user.team_id = this.props.team_id;
|
||||
props.user.email = this.props.email;
|
||||
props.hash = this.props.hash;
|
||||
props.data = this.props.data;
|
||||
props.original_email = this.props.email;
|
||||
}
|
||||
|
||||
return props ;
|
||||
},
|
||||
render: function() {
|
||||
|
||||
client.track('signup', 'signup_user_01_welcome');
|
||||
|
||||
if (this.state.wizard == "finished") {
|
||||
return (<div>You've already completed the signup process for this invitation or this invitation has expired.</div>);
|
||||
}
|
||||
|
||||
var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null;
|
||||
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
|
||||
var password_error = this.state.password_error ? <label className='control-label'>{ this.state.password_error }</label> : null;
|
||||
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null;
|
||||
|
||||
var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span>
|
||||
|
||||
var email =
|
||||
<div className={ this.state.original_email == "" ? "" : "hidden"} >
|
||||
<label className="control-label">Email</label>
|
||||
<div className={ email_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="email" ref="email" className="form-control" defaultValue={ this.state.user.email } placeholder="" maxLength="128" />
|
||||
{ email_error }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img className="signup-team-logo" src="/static/images/logo.png" />
|
||||
<h4>Welcome to { config.SiteName }</h4>
|
||||
<p>{"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}</p>
|
||||
<label className="control-label">Username</label>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" />
|
||||
{ name_error }
|
||||
</div>
|
||||
{ email }
|
||||
<label className="control-label">Password</label>
|
||||
<div className={ name_error ? "form-group has-error" : "form-group" }>
|
||||
<input type="password" ref="password" className="form-control" placeholder="" maxLength="128" />
|
||||
{ password_error }
|
||||
</div>
|
||||
<p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}</p>
|
||||
<p className={ this.state.original_email == "" ? "hidden" : ""}>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p>
|
||||
<div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div>
|
||||
<p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p>
|
||||
{ server_error }
|
||||
<p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
78
web/react/components/team_members.jsx
Обычный файл
78
web/react/components/team_members.jsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var MemberListTeam = require('./member_list_team.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
var users = UserStore.getProfiles();
|
||||
var member_list = [];
|
||||
for (var id in users) member_list.push(users[id]);
|
||||
|
||||
member_list.sort(function(a,b) {
|
||||
if (a.username < b.username) return -1;
|
||||
if (a.username > b.username) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
member_list: member_list
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
UserStore.addChangeListener(this._onChange);
|
||||
|
||||
var self = this;
|
||||
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) {
|
||||
self.setState({ render_members: false });
|
||||
});
|
||||
|
||||
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) {
|
||||
self.setState({ render_members: true });
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
UserStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function() {
|
||||
var newState = getStateFromStores();
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
render: function() {
|
||||
var server_error = this.state.server_error ? <label className='has-error control-label'>{this.state.server_error}</label> : null;
|
||||
|
||||
return (
|
||||
<div className="modal fade" ref="modal" id="team_members" tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close" data-reactid=".5.0.0.0.0"><span aria-hidden="true" data-reactid=".5.0.0.0.0.0">×</span></button>
|
||||
<h4 className="modal-title" id="myModalLabel">{this.props.teamName + " Members"}</h4>
|
||||
</div>
|
||||
<div ref="modalBody" className="modal-body">
|
||||
<div className="channel-settings">
|
||||
<div className="team-member-list">
|
||||
{ this.state.render_members ? <MemberListTeam users={this.state.member_list} /> : "" }
|
||||
</div>
|
||||
{ server_error }
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
290
web/react/components/textbox.jsx
Обычный файл
290
web/react/components/textbox.jsx
Обычный файл
@@ -0,0 +1,290 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SocketStore = require('../stores/socket_store.jsx');
|
||||
var MsgTyping = require('./msg_typing.jsx');
|
||||
var MentionList = require('./mention_list.jsx');
|
||||
var CommandList = require('./command_list.jsx');
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
module.exports = React.createClass({
|
||||
caret: -1,
|
||||
addedMention: false,
|
||||
doProcessMentions: false,
|
||||
mentions: [],
|
||||
componentDidMount: function() {
|
||||
PostStore.addAddMentionListener(this._onChange);
|
||||
|
||||
this.resize();
|
||||
this.processMentions();
|
||||
this.updateTextdiv();
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
PostStore.removeAddMentionListener(this._onChange);
|
||||
},
|
||||
_onChange: function(id, username) {
|
||||
if (id !== this.props.id) return;
|
||||
this.addMention(username);
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
if (this.caret >= 0) {
|
||||
utils.setCaretPosition(this.refs.message.getDOMNode(), this.caret)
|
||||
this.caret = -1;
|
||||
}
|
||||
if (this.doProcessMentions) {
|
||||
this.processMentions();
|
||||
this.doProcessMentions = false;
|
||||
}
|
||||
this.updateTextdiv();
|
||||
this.resize();
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (!this.addedMention) {
|
||||
this.checkForNewMention(nextProps.messageText);
|
||||
}
|
||||
var text = this.refs.message.getDOMNode().value;
|
||||
if (nextProps.channelId != this.props.channelId || nextProps.messageText !== text) {
|
||||
this.doProcessMentions = true;
|
||||
}
|
||||
this.addedMention = false;
|
||||
this.refs.commands.getSuggestedCommands(nextProps.messageText);
|
||||
this.resize();
|
||||
},
|
||||
getInitialState: function() {
|
||||
return { mentionText: '-1', mentions: [] };
|
||||
},
|
||||
updateMentionTab: function(mentionText, excludeList) {
|
||||
var self = this;
|
||||
// using setTimeout so dispatch isn't called during an in progress dispatch
|
||||
setTimeout(function() {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.RECIEVED_MENTION_DATA,
|
||||
id: self.props.id,
|
||||
mention_text: mentionText,
|
||||
exclude_list: excludeList
|
||||
});
|
||||
}, 1);
|
||||
},
|
||||
updateTextdiv: function() {
|
||||
var html = utils.insertHtmlEntities(this.refs.message.getDOMNode().value);
|
||||
for (var k in this.mentions) {
|
||||
var m = this.mentions[k];
|
||||
var re = new RegExp('( |^)@' + m + '( |$|\n)', 'm');
|
||||
html = html.replace(re, '$1<span class="mention">@'+m+'</span>$2');
|
||||
}
|
||||
$(this.refs.textdiv.getDOMNode()).html(html);
|
||||
},
|
||||
handleChange: function() {
|
||||
this.props.onUserInput(this.refs.message.getDOMNode().value);
|
||||
this.resize();
|
||||
},
|
||||
handleKeyPress: function(e) {
|
||||
var text = this.refs.message.getDOMNode().value;
|
||||
|
||||
if (!this.refs.commands.isEmpty() && text.indexOf("/") == 0 && e.which==13) {
|
||||
this.refs.commands.addFirstCommand();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !this.doProcessMentions) {
|
||||
var caret = utils.getCaretPosition(this.refs.message.getDOMNode());
|
||||
var preText = text.substring(0, caret);
|
||||
var lastSpace = preText.lastIndexOf(' ');
|
||||
var lastAt = preText.lastIndexOf('@');
|
||||
|
||||
if (caret > lastAt && lastSpace < lastAt) {
|
||||
this.doProcessMentions = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onKeyPress(e);
|
||||
},
|
||||
handleKeyDown: function(e) {
|
||||
if (utils.getSelectedText(this.refs.message.getDOMNode()) !== '') {
|
||||
this.doProcessMentions = true;
|
||||
}
|
||||
|
||||
if (e.keyCode === 8) {
|
||||
this.handleBackspace(e);
|
||||
}
|
||||
},
|
||||
handleBackspace: function(e) {
|
||||
var text = this.refs.message.getDOMNode().value;
|
||||
if (text.indexOf("/") == 0) {
|
||||
this.refs.commands.getSuggestedCommands(text.substring(0, text.length-1));
|
||||
}
|
||||
|
||||
if (this.doProcessMentions) return;
|
||||
|
||||
var caret = utils.getCaretPosition(this.refs.message.getDOMNode());
|
||||
var preText = text.substring(0, caret);
|
||||
var lastSpace = preText.lastIndexOf(' ');
|
||||
var lastAt = preText.lastIndexOf('@');
|
||||
|
||||
if (caret > lastAt && (lastSpace > lastAt || lastSpace === -1)) {
|
||||
this.doProcessMentions = true;
|
||||
}
|
||||
},
|
||||
processMentions: function() {
|
||||
/* First, find all the possible mentions, highlight them in the HTML and add
|
||||
them all to a list of mentions */
|
||||
var text = utils.insertHtmlEntities(this.refs.message.getDOMNode().value);
|
||||
|
||||
var profileMap = UserStore.getProfilesUsernameMap();
|
||||
|
||||
var re1 = /@([a-z0-9_]+)( |$|\n)/gi;
|
||||
|
||||
var matches = text.match(re1);
|
||||
|
||||
if (!matches) {
|
||||
$(this.refs.textdiv.getDOMNode()).text(text);
|
||||
this.updateMentionTab(null, []);
|
||||
this.mentions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var mentions = [];
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
var m = matches[i].substring(1,matches[i].length).trim();
|
||||
if (m in profileMap && mentions.indexOf(m) === -1) {
|
||||
mentions.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
/* Figure out what the user is currently typing. If it's a mention then we don't
|
||||
want to highlight it and add it to the mention list yet, so we remove it if
|
||||
there is only one occurence of that mention so far. */
|
||||
var caret = utils.getCaretPosition(this.refs.message.getDOMNode());
|
||||
|
||||
var text = this.props.messageText;
|
||||
|
||||
var preText = text.substring(0, caret);
|
||||
|
||||
var atIndex = preText.lastIndexOf('@');
|
||||
var spaceIndex = preText.lastIndexOf(' ');
|
||||
var newLineIndex = preText.lastIndexOf('\n');
|
||||
|
||||
var typingMention = "";
|
||||
if (atIndex > spaceIndex && atIndex > newLineIndex) {
|
||||
|
||||
typingMention = text.substring(atIndex+1, caret);
|
||||
}
|
||||
|
||||
var re3 = new RegExp('@' + typingMention + '( |$|\n)', 'g');
|
||||
|
||||
if ((text.match(re3) || []).length === 1 && mentions.indexOf(typingMention) !== -1) {
|
||||
mentions.splice(mentions.indexOf(typingMention), 1);
|
||||
}
|
||||
|
||||
this.updateMentionTab(null, mentions);
|
||||
this.mentions = mentions;
|
||||
},
|
||||
checkForNewMention: function(text) {
|
||||
var caret = utils.getCaretPosition(this.refs.message.getDOMNode());
|
||||
|
||||
var preText = text.substring(0, caret);
|
||||
|
||||
var atIndex = preText.lastIndexOf('@');
|
||||
|
||||
// The @ character not typed, so nothing to do.
|
||||
if (atIndex === -1) {
|
||||
this.updateMentionTab('-1', null);
|
||||
return;
|
||||
}
|
||||
|
||||
var lastCharSpace = preText.lastIndexOf(String.fromCharCode(160));
|
||||
var lastSpace = preText.lastIndexOf(' ');
|
||||
|
||||
// If there is a space after the last @, nothing to do.
|
||||
if (lastSpace > atIndex || lastCharSpace > atIndex) {
|
||||
this.setState({ mentionText: '-1' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the name typed so far.
|
||||
var name = preText.substring(atIndex+1, preText.length).toLowerCase();
|
||||
this.updateMentionTab(name, null);
|
||||
},
|
||||
addMention: function(name) {
|
||||
var caret = utils.getCaretPosition(this.refs.message.getDOMNode());
|
||||
|
||||
var text = this.props.messageText;
|
||||
|
||||
var preText = text.substring(0, caret);
|
||||
|
||||
var atIndex = preText.lastIndexOf('@');
|
||||
|
||||
// The @ character not typed, so nothing to do.
|
||||
if (atIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = text.substring(0, atIndex);
|
||||
var suffix = text.substring(caret, text.length);
|
||||
this.caret = prefix.length + name.length + 2;
|
||||
this.addedMention = true;
|
||||
this.doProcessMentions = true;
|
||||
|
||||
this.props.onUserInput(prefix + "@" + name + " " + suffix);
|
||||
},
|
||||
addCommand: function(cmd) {
|
||||
var elm = this.refs.message.getDOMNode();
|
||||
elm.value = cmd;
|
||||
this.handleChange();
|
||||
},
|
||||
scroll: function() {
|
||||
var e = this.refs.message.getDOMNode();
|
||||
var d = this.refs.textdiv.getDOMNode();
|
||||
$(d).scrollTop($(e).scrollTop());
|
||||
},
|
||||
resize: function() {
|
||||
var e = this.refs.message.getDOMNode();
|
||||
var w = this.refs.wrapper.getDOMNode();
|
||||
var d = this.refs.textdiv.getDOMNode();
|
||||
|
||||
var lht = parseInt($(e).css('lineHeight'),10);
|
||||
var lines = e.scrollHeight / lht;
|
||||
var mod = lines < 2.5 || this.props.messageText === "" ? 30 : 15;
|
||||
|
||||
if (e.scrollHeight - mod < 167) {
|
||||
$(e).css({'height':'auto','overflow-y':'hidden'}).height(e.scrollHeight - mod);
|
||||
$(d).css({'height':'auto','overflow-y':'hidden'}).height(e.scrollHeight - mod);
|
||||
$(w).css({'height':'auto'}).height(e.scrollHeight+2);
|
||||
} else {
|
||||
$(e).css({'height':'auto','overflow-y':'scroll'}).height(167);
|
||||
$(d).css({'height':'auto','overflow-y':'scroll'}).height(167);
|
||||
$(w).css({'height':'auto'}).height(167);
|
||||
}
|
||||
},
|
||||
handleFocus: function() {
|
||||
var elm = this.refs.message.getDOMNode();
|
||||
if (elm.title === elm.value) {
|
||||
elm.value = "";
|
||||
}
|
||||
},
|
||||
handleBlur: function() {
|
||||
var elm = this.refs.message.getDOMNode();
|
||||
if (elm.value === '') {
|
||||
elm.value = elm.title;
|
||||
}
|
||||
},
|
||||
handlePaste: function() {
|
||||
this.doProcessMentions = true;
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div ref="wrapper" className="textarea-wrapper">
|
||||
<CommandList ref='commands' addCommand={this.addCommand} channelId={this.props.channelId} />
|
||||
<div className="form-control textarea-div" ref="textdiv"/>
|
||||
<textarea id={this.props.id} ref="message" className="form-control custom-textarea" spellCheck="true" autoComplete="off" autoCorrect="off" rows="1" placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onScroll={this.scroll} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
71
web/react/components/user_profile.jsx
Обычный файл
71
web/react/components/user_profile.jsx
Обычный файл
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
function getStateFromStores(userId) {
|
||||
var profile = UserStore.getProfile(userId);
|
||||
|
||||
if (profile == null) {
|
||||
return { profile: { id: "0", username: "..."} };
|
||||
}
|
||||
else {
|
||||
return { profile: profile };
|
||||
}
|
||||
}
|
||||
|
||||
var id = 0;
|
||||
|
||||
function nextId() {
|
||||
id = id + 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
uniqueId: null,
|
||||
componentDidMount: function() {
|
||||
UserStore.addChangeListener(this._onChange);
|
||||
$("#profile_" + this.uniqueId).popover({placement : 'right', container: 'body', trigger: 'hover', html: true, delay: { "show": 200, "hide": 100 }});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
UserStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
_onChange: function(id) {
|
||||
if (id == this.props.userId) {
|
||||
var newState = getStateFromStores(this.props.userId);
|
||||
if (!utils.areStatesEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (this.props.userId != nextProps.userId) {
|
||||
this.setState(getStateFromStores(nextProps.userId));
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
this.uniqueId = nextId();
|
||||
return getStateFromStores(this.props.userId);
|
||||
},
|
||||
render: function() {
|
||||
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
|
||||
|
||||
|
||||
var data_content = ""
|
||||
data_content += "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />"
|
||||
if (!config.ShowEmail) {
|
||||
data_content += "<div><span style='white-space:nowrap;'>Email not shared</span></div>";
|
||||
} else {
|
||||
data_content += "<div><a href='mailto:'" + this.state.profile.email + "'' style='white-space:nowrap;text-transform:lowercase;'>" + this.state.profile.email + "</a></div>";
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{"cursor" : "pointer", "display" : "inline-block"}} className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} >
|
||||
{ name }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
1151
web/react/components/user_settings.jsx
Обычный файл
1151
web/react/components/user_settings.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
189
web/react/components/view_image.jsx
Обычный файл
189
web/react/components/view_image.jsx
Обычный файл
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
|
||||
module.exports = React.createClass({
|
||||
handleNext: function() {
|
||||
var id = this.state.imgId + 1;
|
||||
if (id > this.props.filenames.length-1) {
|
||||
id = 0;
|
||||
}
|
||||
this.setState({ imgId: id });
|
||||
this.loadImage(id);
|
||||
},
|
||||
handlePrev: function() {
|
||||
var id = this.state.imgId - 1;
|
||||
if (id < 0) {
|
||||
id = this.props.filenames.length-1;
|
||||
}
|
||||
this.setState({ imgId: id });
|
||||
this.loadImage(id);
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState({ imgId: nextProps.startId });
|
||||
},
|
||||
loadImage: function(id) {
|
||||
if (this.state.loaded[id] || this.state.images[id]) return;
|
||||
|
||||
var src = "";
|
||||
if (this.props.imgCount > 0) {
|
||||
src = this.props.filenames[id];
|
||||
} else {
|
||||
var fileInfo = utils.splitFileLocation(this.props.filenames[id]);
|
||||
src = fileInfo['path'] + '_preview.jpg';
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var img = new Image();
|
||||
img.load(src,
|
||||
function(){
|
||||
var progress = self.state.progress;
|
||||
progress[id] = img.completedPercentage;
|
||||
self.setState({ progress: progress });
|
||||
});
|
||||
img.onload = function(imgid) {
|
||||
return function() {
|
||||
var loaded = self.state.loaded;
|
||||
loaded[imgid] = true;
|
||||
self.setState({ loaded: loaded });
|
||||
};
|
||||
}(id);
|
||||
var images = this.state.images;
|
||||
images[id] = img;
|
||||
this.setState({ images: images });
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.image) {
|
||||
var height = $(window).height()-100;
|
||||
if (this.state.loaded[this.state.imgId]) {
|
||||
$(this.refs.imageWrap.getDOMNode()).removeClass("default");
|
||||
$(this.refs.image.getDOMNode()).css("max-height",height);
|
||||
}
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
$("#"+this.props.modalId).on('shown.bs.modal', function() {
|
||||
self.setState({ viewed: true });
|
||||
self.loadImage(self.state.imgId);
|
||||
})
|
||||
|
||||
$(this.refs.modal.getDOMNode()).click(function(e){
|
||||
if (e.target == this || e.target == self.refs.imageBody.getDOMNode()) {
|
||||
$('.image_modal').modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$(this.refs.imageWrap.getDOMNode()).hover(
|
||||
function() {
|
||||
$(self.refs.imageFooter.getDOMNode()).addClass("footer--show");
|
||||
}, function() {
|
||||
$(self.refs.imageFooter.getDOMNode()).removeClass("footer--show");
|
||||
}
|
||||
);
|
||||
},
|
||||
getPublicLink: function(e) {
|
||||
data = {};
|
||||
data["channel_id"] = this.props.channelId;
|
||||
data["user_id"] = this.props.userId;
|
||||
data["filename"] = this.props.filenames[this.state.imgId];
|
||||
Client.getPublicLink(data,
|
||||
function(data) {
|
||||
window.open(data["public_link"]);
|
||||
}.bind(this),
|
||||
function(err) {
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getInitialState: function() {
|
||||
var loaded = [];
|
||||
var progress = [];
|
||||
for (var i = 0; i < this.props.filenames.length; i ++) {
|
||||
loaded.push(false);
|
||||
progress.push(0);
|
||||
}
|
||||
return { imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {} };
|
||||
},
|
||||
render: function() {
|
||||
if (this.props.filenames.length < 1 || this.props.filenames.length-1 < this.state.imgId) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
var fileInfo = utils.splitFileLocation(this.props.filenames[this.state.imgId]);
|
||||
|
||||
var name = fileInfo['name'] + '.' + fileInfo['ext'];
|
||||
|
||||
var loading = "";
|
||||
var bgClass = "";
|
||||
var img = {};
|
||||
if (!this.state.loaded[this.state.imgId]) {
|
||||
var percentage = Math.floor(this.state.progress[this.state.imgId]);
|
||||
loading = (
|
||||
<div key={name+"_loading"}>
|
||||
<img ref="placeholder" className="loader-image" src="/static/images/load.gif" />
|
||||
{ percentage > 0 ?
|
||||
<span className="loader-percent" >{"Downloading " + percentage + "%"}</span>
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
bgClass = "black-bg";
|
||||
} else if (this.state.viewed) {
|
||||
for (var id in this.state.images) {
|
||||
var info = utils.splitFileLocation(this.props.filenames[id]);
|
||||
var preview_filename = "";
|
||||
if (this.props.imgCount > 0) {
|
||||
preview_filename = this.props.filenames[this.state.imgId];
|
||||
} else {
|
||||
preview_filename = info['path'] + '_preview.jpg';
|
||||
}
|
||||
|
||||
var imgClass = "hidden";
|
||||
if (this.state.loaded[id] && this.state.imgId == id) imgClass = "";
|
||||
|
||||
img[info['path']] = <a key={info['path']} className={imgClass} href={this.props.filenames[id]} target="_blank"><img ref="image" src={preview_filename}/></a>;
|
||||
}
|
||||
}
|
||||
|
||||
var imgFragment = React.addons.createFragment(img);
|
||||
|
||||
return (
|
||||
<div className="modal fade image_modal" ref="modal" id={this.props.modalId} tabIndex="-1" role="dialog" aria-hidden="true">
|
||||
<div className="modal-dialog modal-image">
|
||||
<div className="modal-content image-content">
|
||||
<div ref="imageBody" className="modal-body image-body">
|
||||
<div ref="imageWrap" className={"image-wrapper default " + bgClass}>
|
||||
<div className="modal-close" data-dismiss="modal"></div>
|
||||
{imgFragment}
|
||||
<div ref="imageFooter" className="modal-button-bar">
|
||||
<span className="pull-left text">{"Image "+(this.state.imgId+1)+" of "+this.props.filenames.length}</span>
|
||||
<div className="image-links">
|
||||
{ config.AllowPublicLink ?
|
||||
<div>
|
||||
<a href="#" className="text" data-title="Public Image" onClick={this.getPublicLink}>Get Public Link</a>
|
||||
<span className="text"> | </span>
|
||||
</div>
|
||||
: "" }
|
||||
<a href={this.props.filenames[id]} download={name} className="text">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
{loading}
|
||||
</div>
|
||||
{ this.props.filenames.length > 1 ?
|
||||
<a className="modal-prev-bar" href="#" onClick={this.handlePrev}>
|
||||
<i className="image-control image-prev"/>
|
||||
</a>
|
||||
: "" }
|
||||
{ this.props.filenames.length > 1 ?
|
||||
<a className="modal-next-bar" href="#" onClick={this.handleNext}>
|
||||
<i className="image-control image-next"/>
|
||||
</a>
|
||||
: "" }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
30
web/react/dispatcher/app_dispatcher.jsx
Обычный файл
30
web/react/dispatcher/app_dispatcher.jsx
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Dispatcher = require('flux').Dispatcher;
|
||||
var assign = require('object-assign');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var PayloadSources = Constants.PayloadSources;
|
||||
|
||||
var AppDispatcher = assign(new Dispatcher(), {
|
||||
|
||||
handleServerAction: function(action) {
|
||||
var payload = {
|
||||
source: PayloadSources.SERVER_ACTION,
|
||||
action: action
|
||||
};
|
||||
this.dispatch(payload);
|
||||
},
|
||||
|
||||
handleViewAction: function(action) {
|
||||
var payload = {
|
||||
source: PayloadSources.VIEW_ACTION,
|
||||
action: action
|
||||
};
|
||||
this.dispatch(payload);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = AppDispatcher;
|
||||
37
web/react/package.json
Обычный файл
37
web/react/package.json
Обычный файл
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mattermost",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"flux": "^2.0.0",
|
||||
"keymirror": "~0.1.0",
|
||||
"object-assign": "^1.0.0",
|
||||
"react": "^0.12.0",
|
||||
"autolinker": "^0.15.2",
|
||||
"linkify-it": "^1.1.0",
|
||||
"react-zeroclipboard-mixin": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^6.2.0",
|
||||
"envify": "^3.0.0",
|
||||
"jest-cli": "~0.1.17",
|
||||
"reactify": "^0.15.2",
|
||||
"uglify-js": "~2.4.15",
|
||||
"watchify": "^2.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",
|
||||
"build": "NODE_ENV=production browserify ./**/*.jsx | uglifyjs > ../static/js/bundle.min.js",
|
||||
"build_old": "NODE_ENV=production browserify ./**/*.jsx | uglifyjs -cm > ../static/js/bundle.min.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"reactify",
|
||||
"envify"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
197
web/react/pages/channel.jsx
Обычный файл
197
web/react/pages/channel.jsx
Обычный файл
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Navbar = require('../components/navbar.jsx');
|
||||
var Sidebar = require('../components/sidebar.jsx');
|
||||
var ChannelHeader = require('../components/channel_header.jsx');
|
||||
var PostList = require('../components/post_list.jsx');
|
||||
var CreatePost = require('../components/create_post.jsx');
|
||||
var SidebarRight = require('../components/sidebar_right.jsx');
|
||||
var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
|
||||
var GetLinkModal = require('../components/get_link_modal.jsx');
|
||||
var MemberInviteModal = require('../components/invite_member_modal.jsx');
|
||||
var EditChannelModal = require('../components/edit_channel_modal.jsx');
|
||||
var DeleteChannelModal = require('../components/delete_channel_modal.jsx');
|
||||
var RenameChannelModal = require('../components/rename_channel_modal.jsx');
|
||||
var RenameTeamModal = require('../components/rename_team_modal.jsx');
|
||||
var EditPostModal = require('../components/edit_post_modal.jsx');
|
||||
var DeletePostModal = require('../components/delete_post_modal.jsx');
|
||||
var MoreChannelsModal = require('../components/more_channels.jsx');
|
||||
var NewChannelModal = require('../components/new_channel.jsx');
|
||||
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
|
||||
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
|
||||
var UserSettingsModal = require('../components/settings_modal.jsx');
|
||||
var ChannelMembersModal = require('../components/channel_members.jsx');
|
||||
var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
|
||||
var TeamMembersModal = require('../components/team_members.jsx');
|
||||
var DirectChannelModal = require('../components/more_direct_channels.jsx');
|
||||
var ErrorBar = require('../components/error_bar.jsx')
|
||||
var ChannelLoader = require('../components/channel_loader.jsx');
|
||||
var MentionList = require('../components/mention_list.jsx');
|
||||
var ChannelInfoModal = require('../components/channel_info_modal.jsx');
|
||||
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
global.window.setup_channel_page = function(team_name, team_type, channel_name, channel_id) {
|
||||
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.CLICK_CHANNEL,
|
||||
name: channel_name,
|
||||
id: channel_id
|
||||
});
|
||||
|
||||
React.render(
|
||||
<ErrorBar/>,
|
||||
document.getElementById('error_bar')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelLoader/>,
|
||||
document.getElementById('channel_loader')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<Navbar teamName={team_name} />,
|
||||
document.getElementById('navbar')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<Sidebar teamName={team_name} teamType={team_type} />,
|
||||
document.getElementById('sidebar-left')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<RenameTeamModal teamName={team_name} />,
|
||||
document.getElementById('rename_team_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<GetLinkModal />,
|
||||
document.getElementById('get_link_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<UserSettingsModal />,
|
||||
document.getElementById('user_settings_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<TeamMembersModal teamName={team_name} />,
|
||||
document.getElementById('team_members_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<MemberInviteModal />,
|
||||
document.getElementById('invite_member_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelHeader />,
|
||||
document.getElementById('channel-header')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<EditChannelModal />,
|
||||
document.getElementById('edit_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<DeleteChannelModal />,
|
||||
document.getElementById('delete_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<RenameChannelModal />,
|
||||
document.getElementById('rename_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelNotificationsModal />,
|
||||
document.getElementById('channel_notifications_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelMembersModal />,
|
||||
document.getElementById('channel_members_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelInviteModal />,
|
||||
document.getElementById('channel_invite_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<ChannelInfoModal />,
|
||||
document.getElementById('channel_info_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<MoreChannelsModal />,
|
||||
document.getElementById('more_channels_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<DirectChannelModal />,
|
||||
document.getElementById('direct_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<NewChannelModal />,
|
||||
document.getElementById('new_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<PostList />,
|
||||
document.getElementById('post-list')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<EditPostModal />,
|
||||
document.getElementById('edit_post_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<DeletePostModal />,
|
||||
document.getElementById('delete_post_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<PostDeletedModal />,
|
||||
document.getElementById('post_deleted_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<CreatePost />,
|
||||
document.getElementById('post-create')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<SidebarRight />,
|
||||
document.getElementById('sidebar-right')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<SidebarRightMenu teamName={team_name} teamType={team_type} />,
|
||||
document.getElementById('sidebar-menu')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<MentionList id="post_textbox" />,
|
||||
document.getElementById('post_mention_tab')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<MentionList id="reply_textbox" />,
|
||||
document.getElementById('reply_mention_tab')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<MentionList id="edit_textbox" />,
|
||||
document.getElementById('edit_mention_tab')
|
||||
);
|
||||
|
||||
};
|
||||
13
web/react/pages/find_team.jsx
Обычный файл
13
web/react/pages/find_team.jsx
Обычный файл
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var FindTeam = require('../components/find_team.jsx');
|
||||
|
||||
global.window.setup_find_team_page = function() {
|
||||
|
||||
React.render(
|
||||
<FindTeam />,
|
||||
document.getElementById('find-team')
|
||||
);
|
||||
|
||||
};
|
||||
14
web/react/pages/home.jsx
Обычный файл
14
web/react/pages/home.jsx
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
|
||||
global.window.setup_home_page = function() {
|
||||
var last = ChannelStore.getLastVisitedName();
|
||||
if (last == null || last.length === 0) {
|
||||
window.location.replace("/channels/" + Constants.DEFAULT_CHANNEL);
|
||||
} else {
|
||||
window.location.replace("/channels/" + last);
|
||||
}
|
||||
}
|
||||
11
web/react/pages/login.jsx
Обычный файл
11
web/react/pages/login.jsx
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Login = require('../components/login.jsx');
|
||||
|
||||
global.window.setup_login_page = function() {
|
||||
React.render(
|
||||
<Login />,
|
||||
document.getElementById('login')
|
||||
);
|
||||
};
|
||||
19
web/react/pages/password_reset.jsx
Обычный файл
19
web/react/pages/password_reset.jsx
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PasswordReset = require('../components/password_reset.jsx');
|
||||
|
||||
global.window.setup_password_reset_page = function(is_reset, team_name, domain, hash, data) {
|
||||
|
||||
React.render(
|
||||
<PasswordReset
|
||||
isReset={is_reset}
|
||||
teamName={team_name}
|
||||
domain={domain}
|
||||
hash={hash}
|
||||
data={data}
|
||||
/>,
|
||||
document.getElementById('reset')
|
||||
);
|
||||
|
||||
};
|
||||
11
web/react/pages/signup_team.jsx
Обычный файл
11
web/react/pages/signup_team.jsx
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SignupTeam =require('../components/signup_team.jsx');
|
||||
|
||||
global.window.setup_signup_team_page = function() {
|
||||
React.render(
|
||||
<SignupTeam />,
|
||||
document.getElementById('signup-team')
|
||||
);
|
||||
};
|
||||
11
web/react/pages/signup_team_complete.jsx
Обычный файл
11
web/react/pages/signup_team_complete.jsx
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SignupTeamComplete =require('../components/signup_team_complete.jsx');
|
||||
|
||||
global.window.setup_signup_team_complete_page = function(email, name, data, hash) {
|
||||
React.render(
|
||||
<SignupTeamComplete name={name} email={email} hash={hash} data={data} />,
|
||||
document.getElementById('signup-team-complete')
|
||||
);
|
||||
};
|
||||
11
web/react/pages/signup_user_complete.jsx
Обычный файл
11
web/react/pages/signup_user_complete.jsx
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SignupUserComplete =require('../components/signup_user_complete.jsx');
|
||||
|
||||
global.window.setup_signup_user_complete_page = function(email, domain, name, id, data, hash) {
|
||||
React.render(
|
||||
<SignupUserComplete team_id={id} domain={domain} team_name={name} email={email} hash={hash} data={data} />,
|
||||
document.getElementById('signup-user-complete')
|
||||
);
|
||||
};
|
||||
13
web/react/pages/verify.jsx
Обычный файл
13
web/react/pages/verify.jsx
Обычный файл
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var EmailVerify = require('../components/email_verify.jsx');
|
||||
|
||||
global.window.setup_verify_page = function(is_verified) {
|
||||
|
||||
React.render(
|
||||
<EmailVerify isVerified={is_verified} />,
|
||||
document.getElementById('verify')
|
||||
);
|
||||
|
||||
};
|
||||
255
web/react/stores/channel_store.jsx
Обычный файл
255
web/react/stores/channel_store.jsx
Обычный файл
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assign = require('object-assign');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var MORE_CHANGE_EVENT = 'change';
|
||||
var EXTRA_INFO_EVENT = 'extra_info';
|
||||
|
||||
var ChannelStore = assign({}, EventEmitter.prototype, {
|
||||
emitChange: function() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
},
|
||||
addChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
},
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
},
|
||||
emitMoreChange: function() {
|
||||
this.emit(MORE_CHANGE_EVENT);
|
||||
},
|
||||
addMoreChangeListener: function(callback) {
|
||||
this.on(MORE_CHANGE_EVENT, callback);
|
||||
},
|
||||
removeMoreChangeListener: function(callback) {
|
||||
this.removeListener(MORE_CHANGE_EVENT, callback);
|
||||
},
|
||||
emitExtraInfoChange: function() {
|
||||
this.emit(EXTRA_INFO_EVENT);
|
||||
},
|
||||
addExtraInfoChangeListener: function(callback) {
|
||||
this.on(EXTRA_INFO_EVENT, callback);
|
||||
},
|
||||
removeExtraInfoChangeListener: function(callback) {
|
||||
this.removeListener(EXTRA_INFO_EVENT, callback);
|
||||
},
|
||||
get: function(id) {
|
||||
var current = null;
|
||||
var c = this._getChannels();
|
||||
|
||||
c.some(function(channel) {
|
||||
if (channel.id == id) {
|
||||
current = channel;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return current;
|
||||
},
|
||||
getMember: function(id) {
|
||||
var current = null;
|
||||
return this.getAllMembers()[id];
|
||||
},
|
||||
getByName: function(name) {
|
||||
var current = null;
|
||||
var c = this._getChannels();
|
||||
|
||||
c.some(function(channel) {
|
||||
if (channel.name == name) {
|
||||
current = channel;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
});
|
||||
|
||||
return current;
|
||||
|
||||
},
|
||||
getAll: function() {
|
||||
return this._getChannels();
|
||||
},
|
||||
getAllMembers: function() {
|
||||
return this._getChannelMembers();
|
||||
},
|
||||
getMoreAll: function() {
|
||||
return this._getMoreChannels();
|
||||
},
|
||||
setCurrentId: function(id) {
|
||||
if (id == null)
|
||||
sessionStorage.removeItem("current_channel_id");
|
||||
else
|
||||
sessionStorage.setItem("current_channel_id", id);
|
||||
},
|
||||
setLastVisitedName: function(name) {
|
||||
if (name == null)
|
||||
localStorage.removeItem("last_visited_name");
|
||||
else
|
||||
localStorage.setItem("last_visited_name", name);
|
||||
},
|
||||
getLastVisitedName: function() {
|
||||
return localStorage.getItem("last_visited_name");
|
||||
},
|
||||
resetCounts: function(id) {
|
||||
var cm = this._getChannelMembers();
|
||||
for (var cmid in cm) {
|
||||
if (cm[cmid].channel_id == id) {
|
||||
var c = this.get(id);
|
||||
if (c) {
|
||||
cm[cmid].msg_count = this.get(id).total_msg_count;
|
||||
cm[cmid].mention_count = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._storeChannelMembers(cm);
|
||||
},
|
||||
getCurrentId: function() {
|
||||
return sessionStorage.getItem("current_channel_id");
|
||||
},
|
||||
getCurrent: function() {
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
|
||||
if (currentId != null)
|
||||
return this.get(currentId);
|
||||
else
|
||||
return null;
|
||||
},
|
||||
getCurrentMember: function() {
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
|
||||
if (currentId != null)
|
||||
return this.getAllMembers()[currentId];
|
||||
else
|
||||
return null;
|
||||
},
|
||||
setChannelMember: function(member) {
|
||||
var members = this._getChannelMembers();
|
||||
members[member.channel_id] = member;
|
||||
this._storeChannelMembers(members);
|
||||
this.emitChange();
|
||||
},
|
||||
getCurrentExtraInfo: function() {
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
var extra = null;
|
||||
|
||||
if (currentId != null)
|
||||
extra = this._getExtraInfos()[currentId];
|
||||
|
||||
if (extra == null)
|
||||
extra = {members: []};
|
||||
|
||||
return extra;
|
||||
},
|
||||
getExtraInfo: function(channel_id) {
|
||||
var extra = null;
|
||||
|
||||
if (channel_id != null)
|
||||
extra = this._getExtraInfos()[channel_id];
|
||||
|
||||
if (extra == null)
|
||||
extra = {members: []};
|
||||
|
||||
return extra;
|
||||
},
|
||||
_storeChannels: function(channels) {
|
||||
sessionStorage.setItem("channels", JSON.stringify(channels));
|
||||
},
|
||||
_getChannels: function() {
|
||||
var channels = [];
|
||||
try {
|
||||
channels = JSON.parse(sessionStorage.channels);
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return channels;
|
||||
},
|
||||
_storeChannelMembers: function(channelMembers) {
|
||||
sessionStorage.setItem("channel_members", JSON.stringify(channelMembers));
|
||||
},
|
||||
_getChannelMembers: function() {
|
||||
var members = {};
|
||||
try {
|
||||
members = JSON.parse(sessionStorage.channel_members);
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return members;
|
||||
},
|
||||
_storeMoreChannels: function(channels) {
|
||||
sessionStorage.setItem("more_channels", JSON.stringify(channels));
|
||||
},
|
||||
_getMoreChannels: function() {
|
||||
var channels = [];
|
||||
try {
|
||||
channels = JSON.parse(sessionStorage.more_channels);
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return channels;
|
||||
},
|
||||
_storeExtraInfos: function(extraInfos) {
|
||||
sessionStorage.setItem("extra_infos", JSON.stringify(extraInfos));
|
||||
},
|
||||
_getExtraInfos: function() {
|
||||
var members = {};
|
||||
try {
|
||||
members = JSON.parse(sessionStorage.extra_infos);
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
});
|
||||
|
||||
ChannelStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
var action = payload.action;
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
case ActionTypes.CLICK_CHANNEL:
|
||||
ChannelStore.setCurrentId(action.id);
|
||||
ChannelStore.setLastVisitedName(action.name);
|
||||
ChannelStore.resetCounts(action.id);
|
||||
ChannelStore.emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.RECIEVED_CHANNELS:
|
||||
ChannelStore._storeChannels(action.channels);
|
||||
ChannelStore._storeChannelMembers(action.members);
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
if (currentId) ChannelStore.resetCounts(currentId);
|
||||
ChannelStore.emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.RECIEVED_MORE_CHANNELS:
|
||||
ChannelStore._storeMoreChannels(action.channels);
|
||||
ChannelStore.emitMoreChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO:
|
||||
var extra_infos = ChannelStore._getExtraInfos();
|
||||
extra_infos[action.extra_info.id] = action.extra_info;
|
||||
ChannelStore._storeExtraInfos(extra_infos);
|
||||
ChannelStore.emitExtraInfoChange();
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ChannelStore;
|
||||
59
web/react/stores/error_store.jsx
Обычный файл
59
web/react/stores/error_store.jsx
Обычный файл
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assign = require('object-assign');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
|
||||
var ErrorStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
emitChange: function() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
},
|
||||
|
||||
addChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
},
|
||||
handledError: function() {
|
||||
sessionStorage.removeItem("last_error");
|
||||
},
|
||||
getLastError: function() {
|
||||
var error = null;
|
||||
try {
|
||||
error = JSON.parse(sessionStorage.last_error);
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return error;
|
||||
},
|
||||
|
||||
_storeLastError: function(error) {
|
||||
sessionStorage.setItem("last_error", JSON.stringify(error));
|
||||
},
|
||||
});
|
||||
|
||||
ErrorStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
var action = payload.action;
|
||||
switch(action.type) {
|
||||
case ActionTypes.RECIEVED_ERROR:
|
||||
ErrorStore._storeLastError(action.err);
|
||||
ErrorStore.emitChange();
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ErrorStore;
|
||||
|
||||
|
||||
224
web/react/stores/post_store.jsx
Обычный файл
224
web/react/stores/post_store.jsx
Обычный файл
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assign = require('object-assign');
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
|
||||
var PostStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
emitChange: function() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
},
|
||||
|
||||
addChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
emitSearchChange: function() {
|
||||
this.emit(SEARCH_CHANGE_EVENT);
|
||||
},
|
||||
|
||||
addSearchChangeListener: function(callback) {
|
||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeSearchChangeListener: function(callback) {
|
||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
emitSearchTermChange: function(doSearch, isMentionSearch) {
|
||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||
},
|
||||
|
||||
addSearchTermChangeListener: function(callback) {
|
||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeSearchTermChangeListener: function(callback) {
|
||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
emitSelectedPostChange: function(from_search) {
|
||||
this.emit(SELECTED_POST_CHANGE_EVENT, from_search);
|
||||
},
|
||||
|
||||
addSelectedPostChangeListener: function(callback) {
|
||||
this.on(SELECTED_POST_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeSelectedPostChangeListener: function(callback) {
|
||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
emitMentionDataChange: function(id, mentionText, excludeList) {
|
||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText, excludeList);
|
||||
},
|
||||
|
||||
addMentionDataChangeListener: function(callback) {
|
||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
removeMentionDataChangeListener: function(callback) {
|
||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
},
|
||||
|
||||
emitAddMention: function(id, username) {
|
||||
this.emit(ADD_MENTION_EVENT, id, username);
|
||||
},
|
||||
|
||||
addAddMentionListener: function(callback) {
|
||||
this.on(ADD_MENTION_EVENT, callback);
|
||||
},
|
||||
|
||||
removeAddMentionListener: function(callback) {
|
||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||
},
|
||||
|
||||
getCurrentPosts: function() {
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
|
||||
if (currentId != null)
|
||||
return this.getPosts(currentId);
|
||||
else
|
||||
return null;
|
||||
},
|
||||
storePosts: function(channelId, posts) {
|
||||
this._storePosts(channelId, posts);
|
||||
this.emitChange();
|
||||
},
|
||||
_storePosts: function(channelId, posts) {
|
||||
sessionStorage.setItem("posts_" + channelId, JSON.stringify(posts));
|
||||
},
|
||||
getPosts: function(channelId) {
|
||||
var posts = null;
|
||||
try {
|
||||
posts = JSON.parse(sessionStorage.getItem("posts_" + channelId));
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return posts;
|
||||
},
|
||||
storeSearchResults: function(results, is_mention_search) {
|
||||
sessionStorage.setItem("search_results", JSON.stringify(results));
|
||||
is_mention_search = is_mention_search ? true : false; // force to bool
|
||||
sessionStorage.setItem("is_mention_search", JSON.stringify(is_mention_search));
|
||||
},
|
||||
getSearchResults: function() {
|
||||
var results = null;
|
||||
try {
|
||||
results = JSON.parse(sessionStorage.getItem("search_results"));
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
getIsMentionSearch: function() {
|
||||
var result = false;
|
||||
try {
|
||||
result = JSON.parse(sessionStorage.getItem("is_mention_search"));
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
storeSelectedPost: function(post_list) {
|
||||
sessionStorage.setItem("select_post", JSON.stringify(post_list));
|
||||
},
|
||||
getSelectedPost: function() {
|
||||
var post_list = null;
|
||||
try {
|
||||
post_list = JSON.parse(sessionStorage.getItem("select_post"));
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return post_list;
|
||||
},
|
||||
storeSearchTerm: function(term) {
|
||||
sessionStorage.setItem("search_term", term);
|
||||
},
|
||||
getSearchTerm: function() {
|
||||
return sessionStorage.getItem("search_term");
|
||||
},
|
||||
storeCurrentDraft: function(draft) {
|
||||
var channel_id = ChannelStore.getCurrentId();
|
||||
var user_id = UserStore.getCurrentId();
|
||||
localStorage.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft));
|
||||
},
|
||||
getCurrentDraft: function() {
|
||||
var channel_id = ChannelStore.getCurrentId();
|
||||
var user_id = UserStore.getCurrentId();
|
||||
return JSON.parse(localStorage.getItem("draft_" + channel_id + "_" + user_id));
|
||||
},
|
||||
storeDraft: function(channel_id, user_id, draft) {
|
||||
localStorage.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft));
|
||||
},
|
||||
getDraft: function(channel_id, user_id) {
|
||||
return JSON.parse(localStorage.getItem("draft_" + channel_id + "_" + user_id));
|
||||
},
|
||||
clearDraftUploads: function() {
|
||||
for (key in localStorage) {
|
||||
if (key.substring(0,6) === "draft_") {
|
||||
var d = JSON.parse(localStorage.getItem(key));
|
||||
if (d) {
|
||||
d['uploadsInProgress'] = 0;
|
||||
localStorage.setItem(key, JSON.stringify(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PostStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
var action = payload.action;
|
||||
|
||||
switch(action.type) {
|
||||
case ActionTypes.RECIEVED_POSTS:
|
||||
PostStore._storePosts(action.id, action.post_list);
|
||||
PostStore.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH:
|
||||
PostStore.storeSearchResults(action.results, action.is_mention_search);
|
||||
PostStore.emitSearchChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||
PostStore.storeSearchTerm(action.term);
|
||||
PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_POST_SELECTED:
|
||||
PostStore.storeSelectedPost(action.post_list);
|
||||
PostStore.emitSelectedPostChange(action.from_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||
PostStore.emitMentionDataChange(action.id, action.mention_text, action.exclude_list);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
PostStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PostStore;
|
||||
86
web/react/stores/socket_store.jsx
Обычный файл
86
web/react/stores/socket_store.jsx
Обычный файл
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var UserStore = require('./user_store.jsx')
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assign = require('object-assign');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
|
||||
var conn;
|
||||
|
||||
var SocketStore = assign({}, EventEmitter.prototype, {
|
||||
initialize: function(self) {
|
||||
if (!UserStore.getCurrentId()) return;
|
||||
|
||||
if (!self) self = this;
|
||||
self.setMaxListeners(0);
|
||||
|
||||
if (window["WebSocket"] && !conn) {
|
||||
var protocol = window.location.protocol == "https:" ? "wss://" : "ws://";
|
||||
var port = window.location.protocol == "https:" ? ":8443" : "";
|
||||
var conn_url = protocol + location.host + port + "/api/v1/websocket";
|
||||
console.log("connecting to " + conn_url);
|
||||
conn = new WebSocket(conn_url);
|
||||
|
||||
conn.onclose = function(evt) {
|
||||
console.log("websocket closed");
|
||||
console.log(evt);
|
||||
conn = null;
|
||||
setTimeout(function(){self.initialize(self)}, 3000);
|
||||
};
|
||||
|
||||
conn.onerror = function(evt) {
|
||||
console.log("websocket error");
|
||||
console.log(evt);
|
||||
};
|
||||
|
||||
conn.onmessage = function(evt) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_MSG,
|
||||
msg: JSON.parse(evt.data)
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
emitChange: function(msg) {
|
||||
this.emit(CHANGE_EVENT, msg);
|
||||
},
|
||||
addChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
},
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
},
|
||||
sendMessage: function (msg) {
|
||||
if (conn && conn.readyState === WebSocket.OPEN) {
|
||||
conn.send(JSON.stringify(msg));
|
||||
} else if (!conn || conn.readyState === WebSocket.Closed) {
|
||||
conn = null;
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SocketStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
var action = payload.action;
|
||||
|
||||
switch(action.type) {
|
||||
case ActionTypes.RECIEVED_MSG:
|
||||
SocketStore.emitChange(action.msg);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
SocketStore.initialize();
|
||||
module.exports = SocketStore;
|
||||
|
||||
|
||||
|
||||
|
||||
328
web/react/stores/user_store.jsx
Обычный файл
328
web/react/stores/user_store.jsx
Обычный файл
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assign = require('object-assign');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var CHANGE_EVENT_SESSIONS = 'change_sessions';
|
||||
var CHANGE_EVENT_AUDITS = 'change_audits';
|
||||
var CHANGE_EVENT_TEAMS = 'change_teams';
|
||||
var CHANGE_EVENT_STATUSES = 'change_statuses';
|
||||
|
||||
var UserStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
emitChange: function(userId) {
|
||||
this.emit(CHANGE_EVENT, userId);
|
||||
},
|
||||
addChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
},
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
},
|
||||
emitSessionsChange: function() {
|
||||
this.emit(CHANGE_EVENT_SESSIONS);
|
||||
},
|
||||
addSessionsChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT_SESSIONS, callback);
|
||||
},
|
||||
removeSessionsChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT_SESSIONS, callback);
|
||||
},
|
||||
emitAuditsChange: function() {
|
||||
this.emit(CHANGE_EVENT_AUDITS);
|
||||
},
|
||||
addAuditsChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT_AUDITS, callback);
|
||||
},
|
||||
removeAuditsChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT_AUDITS, callback);
|
||||
},
|
||||
emitTeamsChange: function() {
|
||||
this.emit(CHANGE_EVENT_TEAMS);
|
||||
},
|
||||
addTeamsChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT_TEAMS, callback);
|
||||
},
|
||||
removeTeamsChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT_TEAMS, callback);
|
||||
},
|
||||
emitStatusesChange: function() {
|
||||
this.emit(CHANGE_EVENT_STATUSES);
|
||||
},
|
||||
addStatusesChangeListener: function(callback) {
|
||||
this.on(CHANGE_EVENT_STATUSES, callback);
|
||||
},
|
||||
removeStatusesChangeListener: function(callback) {
|
||||
this.removeListener(CHANGE_EVENT_STATUSES, callback);
|
||||
},
|
||||
setCurrentId: function(id) {
|
||||
if (id == null)
|
||||
sessionStorage.removeItem("current_user_id");
|
||||
else
|
||||
sessionStorage.setItem("current_user_id", id);
|
||||
},
|
||||
getCurrentId: function(skipFetch) {
|
||||
var current_id = sessionStorage.current_user_id;
|
||||
|
||||
// this is a speical case to force fetch the
|
||||
// current user if it's missing
|
||||
// it's synchronous to block rendering
|
||||
if (current_id == null && !skipFetch) {
|
||||
var me = client.getMeSynchronous();
|
||||
if (me != null) {
|
||||
this.setCurrentUser(me);
|
||||
current_id = me.id;
|
||||
}
|
||||
}
|
||||
|
||||
return current_id;
|
||||
},
|
||||
getCurrentUser: function(skipFetch) {
|
||||
if (this.getCurrentId(skipFetch) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._getProfiles()[this.getCurrentId()];
|
||||
},
|
||||
setCurrentUser: function(user) {
|
||||
this.saveProfile(user);
|
||||
this.setCurrentId(user.id);
|
||||
},
|
||||
getLastDomain: function() {
|
||||
return localStorage.last_domain;
|
||||
},
|
||||
setLastDomain: function(domain) {
|
||||
localStorage.setItem("last_domain", domain);
|
||||
},
|
||||
getLastEmail: function() {
|
||||
return localStorage.last_email;
|
||||
},
|
||||
setLastEmail: function(email) {
|
||||
localStorage.setItem("last_email", email);
|
||||
},
|
||||
removeCurrentUser: function() {
|
||||
this.setCurrentId(null);
|
||||
},
|
||||
hasProfile: function(userId) {
|
||||
return this._getProfiles()[userId] != null;
|
||||
},
|
||||
getProfile: function(userId) {
|
||||
return this._getProfiles()[userId];
|
||||
},
|
||||
getProfileByUsername: function(username) {
|
||||
return this._getProfilesUsernameMap()[username];
|
||||
},
|
||||
getProfilesUsernameMap: function() {
|
||||
return this._getProfilesUsernameMap();
|
||||
},
|
||||
getProfiles: function() {
|
||||
|
||||
return this._getProfiles();
|
||||
},
|
||||
getActiveOnlyProfiles: function() {
|
||||
active = {};
|
||||
current = this._getProfiles();
|
||||
|
||||
for (var key in current) {
|
||||
if (current[key].delete_at == 0) {
|
||||
active[key] = current[key];
|
||||
}
|
||||
}
|
||||
|
||||
return active;
|
||||
},
|
||||
saveProfile: function(profile) {
|
||||
var ps = this._getProfiles();
|
||||
ps[profile.id] = profile;
|
||||
this._storeProfiles(ps);
|
||||
},
|
||||
_storeProfiles: function(profiles) {
|
||||
sessionStorage.setItem("profiles", JSON.stringify(profiles));
|
||||
var profileUsernameMap = {};
|
||||
for (var id in profiles) {
|
||||
profileUsernameMap[profiles[id].username] = profiles[id];
|
||||
}
|
||||
sessionStorage.setItem("profileUsernameMap", JSON.stringify(profileUsernameMap));
|
||||
},
|
||||
_getProfiles: function() {
|
||||
var profiles = {};
|
||||
try {
|
||||
profiles = JSON.parse(sessionStorage.getItem("profiles"));
|
||||
|
||||
if (profiles == null) {
|
||||
profiles = {};
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return profiles;
|
||||
},
|
||||
_getProfilesUsernameMap: function() {
|
||||
var profileUsernameMap = {};
|
||||
try {
|
||||
profileUsernameMap = JSON.parse(sessionStorage.getItem("profileUsernameMap"));
|
||||
|
||||
if (profileUsernameMap == null) {
|
||||
profileUsernameMap = {};
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return profileUsernameMap;
|
||||
},
|
||||
setSessions: function(sessions) {
|
||||
sessionStorage.setItem("sessions", JSON.stringify(sessions));
|
||||
},
|
||||
getSessions: function() {
|
||||
var sessions = [];
|
||||
try {
|
||||
sessions = JSON.parse(sessionStorage.getItem("sessions"));
|
||||
|
||||
if (sessions == null) {
|
||||
sessions = [];
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return sessions;
|
||||
},
|
||||
setAudits: function(audits) {
|
||||
sessionStorage.setItem("audits", JSON.stringify(audits));
|
||||
},
|
||||
getAudits: function() {
|
||||
var audits = [];
|
||||
try {
|
||||
audits = JSON.parse(sessionStorage.getItem("audits"));
|
||||
|
||||
if (audits == null) {
|
||||
audits = [];
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return audits;
|
||||
},
|
||||
setTeams: function(teams) {
|
||||
sessionStorage.setItem("teams", JSON.stringify(teams));
|
||||
},
|
||||
getTeams: function() {
|
||||
var teams = [];
|
||||
try {
|
||||
teams = JSON.parse(sessionStorage.getItem("teams"));
|
||||
|
||||
if (teams == null) {
|
||||
teams = [];
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return teams;
|
||||
},
|
||||
getCurrentMentionKeys: function() {
|
||||
var user = this.getCurrentUser();
|
||||
if (user.notify_props && user.notify_props.mention_keys) {
|
||||
var keys = user.notify_props.mention_keys.split(',');
|
||||
|
||||
if (user.full_name.length > 0 && user.notify_props.first_name === "true") {
|
||||
var first = user.full_name.split(' ')[0];
|
||||
if (first.length > 0) keys.push(first);
|
||||
}
|
||||
|
||||
return keys;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getLastVersion: function() {
|
||||
return sessionStorage.last_version;
|
||||
},
|
||||
setLastVersion: function(version) {
|
||||
sessionStorage.setItem("last_version", version);
|
||||
},
|
||||
setStatuses: function(statuses) {
|
||||
this._setStatuses(statuses);
|
||||
this.emitStatusesChange();
|
||||
},
|
||||
_setStatuses: function(statuses) {
|
||||
sessionStorage.setItem("statuses", JSON.stringify(statuses));
|
||||
},
|
||||
setStatus: function(user_id, status) {
|
||||
var statuses = this.getStatuses();
|
||||
statuses[user_id] = status;
|
||||
this._setStatuses(statuses);
|
||||
this.emitStatusesChange();
|
||||
},
|
||||
getStatuses: function() {
|
||||
var statuses = {};
|
||||
try {
|
||||
statuses = JSON.parse(sessionStorage.getItem("statuses"));
|
||||
|
||||
if (statuses == null) {
|
||||
statuses = {};
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
return statuses;
|
||||
},
|
||||
getStatus: function(id) {
|
||||
return this.getStatuses()[id];
|
||||
}
|
||||
});
|
||||
|
||||
UserStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
var action = payload.action;
|
||||
|
||||
switch(action.type) {
|
||||
case ActionTypes.RECIEVED_PROFILES:
|
||||
for(var id in action.profiles) {
|
||||
// profiles can have incomplete data, so don't overwrite current user
|
||||
if (id === UserStore.getCurrentId()) continue;
|
||||
var profile = action.profiles[id];
|
||||
UserStore.saveProfile(profile);
|
||||
UserStore.emitChange(profile.id);
|
||||
}
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ME:
|
||||
UserStore.setCurrentUser(action.me);
|
||||
UserStore.emitChange(action.me.id);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SESSIONS:
|
||||
UserStore.setSessions(action.sessions);
|
||||
UserStore.emitSessionsChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_AUDITS:
|
||||
UserStore.setAudits(action.audits);
|
||||
UserStore.emitAuditsChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_TEAMS:
|
||||
UserStore.setTeams(action.teams);
|
||||
UserStore.emitTeamsChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_STATUSES:
|
||||
UserStore._setStatuses(action.statuses);
|
||||
UserStore.emitStatusesChange();
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
UserStore.setMaxListeners(0);
|
||||
global.window.UserStore = UserStore;
|
||||
module.exports = UserStore;
|
||||
|
||||
|
||||
357
web/react/utils/async_client.jsx
Обычный файл
357
web/react/utils/async_client.jsx
Обычный файл
@@ -0,0 +1,357 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var client = require('./client.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var utils = require('./utils.jsx');
|
||||
|
||||
var Constants = require('./constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
// Used to track in progress async calls
|
||||
var callTracker = {};
|
||||
|
||||
var dispatchError = function(err, method) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_ERROR,
|
||||
err: err,
|
||||
method: method
|
||||
});
|
||||
};
|
||||
|
||||
var isCallInProgress = function(callName) {
|
||||
if (!(callName in callTracker)) return false;
|
||||
|
||||
if (callTracker[callName] === 0) return false;
|
||||
|
||||
if (utils.getTimestamp() - callTracker[callName] > 5000) {
|
||||
console.log("AsyncClient call " + callName + " expired after more than 5 seconds");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports.dispatchError = dispatchError;
|
||||
|
||||
module.exports.getChannels = function(force, updateLastViewed, checkVersion) {
|
||||
if (isCallInProgress("getChannels")) return;
|
||||
|
||||
if (ChannelStore.getAll().length == 0 || force) {
|
||||
callTracker["getChannels"] = utils.getTimestamp();
|
||||
client.getChannels(
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getChannels"] = 0;
|
||||
|
||||
if (updateLastViewed && ChannelStore.getCurrentId() != null) {
|
||||
module.exports.updateLastViewedAt();
|
||||
}
|
||||
|
||||
if (checkVersion) {
|
||||
var serverVersion = xhr.getResponseHeader("X-Version-ID");
|
||||
|
||||
if (UserStore.getLastVersion() == undefined) {
|
||||
UserStore.setLastVersion(serverVersion);
|
||||
}
|
||||
|
||||
if (serverVersion != UserStore.getLastVersion()) {
|
||||
UserStore.setLastVersion(serverVersion);
|
||||
window.location.href = window.location.href;
|
||||
console.log("Detected version update refreshing the page");
|
||||
}
|
||||
}
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_CHANNELS,
|
||||
channels: data.channels,
|
||||
members: data.members
|
||||
});
|
||||
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getChannels"] = 0;
|
||||
dispatchError(err, "getChannels");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.updateLastViewedAt = function() {
|
||||
if (isCallInProgress("updateLastViewed")) return;
|
||||
|
||||
if (ChannelStore.getCurrentId() == null) return;
|
||||
|
||||
callTracker["updateLastViewed"] = utils.getTimestamp();
|
||||
client.updateLastViewedAt(
|
||||
ChannelStore.getCurrentId(),
|
||||
function(data) {
|
||||
callTracker["updateLastViewed"] = 0;
|
||||
},
|
||||
function(err) {
|
||||
callTracker["updateLastViewed"] = 0;
|
||||
dispatchError(err, "updateLastViewedAt");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.getMoreChannels = function(force) {
|
||||
if (isCallInProgress("getMoreChannels")) return;
|
||||
|
||||
if (ChannelStore.getMoreAll().length == 0 || force) {
|
||||
|
||||
callTracker["getMoreChannels"] = utils.getTimestamp();
|
||||
client.getMoreChannels(
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getMoreChannels"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_MORE_CHANNELS,
|
||||
channels: data.channels,
|
||||
members: data.members
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getMoreChannels"] = 0;
|
||||
dispatchError(err, "getMoreChannels");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getChannelExtraInfo = function(force) {
|
||||
var channelId = ChannelStore.getCurrentId();
|
||||
|
||||
if (channelId != null) {
|
||||
if (isCallInProgress("getChannelExtraInfo_"+channelId)) return;
|
||||
var minMembers = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D' ? 1 : 0;
|
||||
|
||||
if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) {
|
||||
callTracker["getChannelExtraInfo_"+channelId] = utils.getTimestamp();
|
||||
client.getChannelExtraInfo(
|
||||
channelId,
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getChannelExtraInfo_"+channelId] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
|
||||
extra_info: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getChannelExtraInfo_"+channelId] = 0;
|
||||
dispatchError(err, "getChannelExtraInfo");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getProfiles = function() {
|
||||
if (isCallInProgress("getProfiles")) return;
|
||||
|
||||
callTracker["getProfiles"] = utils.getTimestamp();
|
||||
client.getProfiles(
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getProfiles"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_PROFILES,
|
||||
profiles: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getProfiles"] = 0;
|
||||
dispatchError(err, "getProfiles");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.getSessions = function() {
|
||||
if (isCallInProgress("getSessions")) return;
|
||||
|
||||
callTracker["getSessions"] = utils.getTimestamp();
|
||||
client.getSessions(
|
||||
UserStore.getCurrentId(),
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getSessions"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SESSIONS,
|
||||
sessions: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getSessions"] = 0;
|
||||
dispatchError(err, "getSessions");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.getAudits = function() {
|
||||
if (isCallInProgress("getAudits")) return;
|
||||
|
||||
callTracker["getAudits"] = utils.getTimestamp();
|
||||
client.getAudits(
|
||||
UserStore.getCurrentId(),
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getAudits"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_AUDITS,
|
||||
audits: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getAudits"] = 0;
|
||||
dispatchError(err, "getAudits");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.findTeams = function(email) {
|
||||
if (isCallInProgress("findTeams_"+email)) return;
|
||||
|
||||
var user = UserStore.getCurrentUser();
|
||||
if (user) {
|
||||
callTracker["findTeams_"+email] = utils.getTimestamp();
|
||||
client.findTeams(
|
||||
user.email,
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["findTeams_"+email] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_TEAMS,
|
||||
teams: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["findTeams_"+email] = 0;
|
||||
dispatchError(err, "findTeams");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.search = function(terms) {
|
||||
if (isCallInProgress("search_"+String(terms))) return;
|
||||
|
||||
callTracker["search_"+String(terms)] = utils.getTimestamp();
|
||||
client.search(
|
||||
terms,
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["search_"+String(terms)] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["search_"+String(terms)] = 0;
|
||||
dispatchError(err, "search");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.getPosts = function(force, id) {
|
||||
if (PostStore.getCurrentPosts() == null || force) {
|
||||
var channelId = id ? id : ChannelStore.getCurrentId();
|
||||
|
||||
if (isCallInProgress("getPosts_"+channelId)) return;
|
||||
|
||||
var post_list = PostStore.getCurrentPosts();
|
||||
// if we already have more than POST_CHUNK_SIZE posts,
|
||||
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
|
||||
// with a max at 180
|
||||
var numPosts = post_list && post_list.order.length > 0 ? Math.min(180, Constants.POST_CHUNK_SIZE * Math.ceil(post_list.order.length / Constants.POST_CHUNK_SIZE)) : Constants.POST_CHUNK_SIZE;
|
||||
|
||||
if (channelId != null) {
|
||||
callTracker["getPosts_"+channelId] = utils.getTimestamp();
|
||||
client.getPosts(
|
||||
channelId,
|
||||
0,
|
||||
numPosts,
|
||||
function(data, textStatus, xhr) {
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POSTS,
|
||||
id: channelId,
|
||||
post_list: data
|
||||
});
|
||||
|
||||
module.exports.getProfiles();
|
||||
},
|
||||
function(err) {
|
||||
dispatchError(err, "getPosts");
|
||||
},
|
||||
function() {
|
||||
callTracker["getPosts_"+channelId] = 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getMe = function() {
|
||||
if (isCallInProgress("getMe")) return;
|
||||
|
||||
callTracker["getMe"] = utils.getTimestamp();
|
||||
client.getMeSynchronous(
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getMe"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_ME,
|
||||
me: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getMe"] = 0;
|
||||
dispatchError(err, "getMe");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.getStatuses = function() {
|
||||
if (isCallInProgress("getStatuses")) return;
|
||||
|
||||
callTracker["getStatuses"] = utils.getTimestamp();
|
||||
client.getStatuses(
|
||||
function(data, textStatus, xhr) {
|
||||
callTracker["getStatuses"] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) return;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_STATUSES,
|
||||
statuses: data
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
callTracker["getStatuses"] = 0;
|
||||
dispatchError(err, "getStatuses");
|
||||
}
|
||||
);
|
||||
}
|
||||
813
web/react/utils/client.jsx
Обычный файл
813
web/react/utils/client.jsx
Обычный файл
@@ -0,0 +1,813 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
|
||||
module.exports.track = function(category, action, label, prop, val) {
|
||||
global.window.snowplow('trackStructEvent', category, action, label, prop, val);
|
||||
if (global.window.analytics != null) global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
|
||||
};
|
||||
|
||||
module.exports.trackPage = function() {
|
||||
global.window.snowplow('trackPageView');
|
||||
if (global.window.analytics != null) global.window.analytics.page();
|
||||
};
|
||||
|
||||
function handleError(method_name, xhr, status, err) {
|
||||
var _LTracker = global.window._LTracker || [];
|
||||
|
||||
var e = null;
|
||||
try {
|
||||
e = JSON.parse(xhr.responseText);
|
||||
}
|
||||
catch(parse_error) {
|
||||
}
|
||||
|
||||
var msg = "";
|
||||
|
||||
if (e) {
|
||||
msg = "error in " + method_name + " msg=" + e.message + " detail=" + e.detailed_error + " rid=" + e.request_id;
|
||||
}
|
||||
else {
|
||||
msg = "error in " + method_name + " status=" + status + " statusCode=" + xhr.status + " err=" + err;
|
||||
|
||||
if (xhr.status === 0)
|
||||
e = { message: "There appears to be a problem with your internet connection" };
|
||||
else
|
||||
e = { message: "We received an unexpected status code from the server (" + xhr.status + ")"};
|
||||
}
|
||||
|
||||
console.error(msg)
|
||||
console.error(e);
|
||||
_LTracker.push(msg);
|
||||
|
||||
module.exports.track('api', 'api_weberror', method_name, 'message', msg);
|
||||
|
||||
if (xhr.status == 401) {
|
||||
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname+window.location.search);
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
module.exports.createTeamFromSignup = function(team_signup, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/create_from_signup",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(team_signup),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("createTeamFromSignup", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createUser = function(user, data, email_hash, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/create?d=" + encodeURIComponent(data) + "&h=" + encodeURIComponent(email_hash),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(user),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("createUser", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_create', user.team_id, 'email', user.email);
|
||||
};
|
||||
|
||||
module.exports.updateUser = function(user, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/update",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(user),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateUser", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_update');
|
||||
};
|
||||
|
||||
module.exports.updatePassword = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/newpassword",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("newPassword", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_newpassword');
|
||||
};
|
||||
|
||||
module.exports.updateUserNotifyProps = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/update_notify",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateUserNotifyProps", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateRoles = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/update_roles",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateRoles", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_update_roles');
|
||||
};
|
||||
|
||||
module.exports.updateActive = function(userId, active, success, error) {
|
||||
var data = {};
|
||||
data["user_id"] = userId;
|
||||
data["active"] = "" + active;
|
||||
|
||||
$.ajax({
|
||||
url: "/api/v1/users/update_active",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateActive", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_update_roles');
|
||||
};
|
||||
|
||||
module.exports.sendPasswordReset = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/send_password_reset",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("sendPasswordReset", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_send_password_reset');
|
||||
};
|
||||
|
||||
module.exports.resetPassword = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/reset_password",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("resetPassword", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_reset_password');
|
||||
};
|
||||
|
||||
module.exports.logout = function() {
|
||||
module.exports.track('api', 'api_users_logout');
|
||||
sessionStorage.clear();
|
||||
window.location.href = "/logout";
|
||||
};
|
||||
|
||||
module.exports.loginByEmail = function(domain, email, password, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/login",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({domain: domain, email: email, password: password}),
|
||||
success: function(data, textStatus, xhr) {
|
||||
module.exports.track('api', 'api_users_login_success', data.team_id, 'email', data.email);
|
||||
success(data, textStatus, xhr);
|
||||
},
|
||||
error: function(xhr, status, err) {
|
||||
module.exports.track('api', 'api_users_login_fail', window.getSubDomain(), 'email', email);
|
||||
|
||||
e = handleError("loginByEmail", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.revokeSession = function(altId, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/revoke_session",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({id: altId}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("revokeSession", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getSessions = function(userId, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/"+userId+"/sessions",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getSessions", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getAudits = function(userId, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/"+userId+"/audits",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getAudits", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getMeSynchronous = function(success, error) {
|
||||
|
||||
var current_user = null;
|
||||
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: "/api/v1/users/me",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: function(data, textStatus, xhr) {
|
||||
current_user = data;
|
||||
if (success) success(data, textStatus, xhr);
|
||||
},
|
||||
error: function(xhr, status, err) {
|
||||
if (error) {
|
||||
e = handleError("getMeSynchronous", xhr, status, err);
|
||||
error(e);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return current_user;
|
||||
};
|
||||
|
||||
module.exports.inviteMembers = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/invite_members",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("inviteMembers", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_teams_invite_members');
|
||||
};
|
||||
|
||||
module.exports.updateTeamName = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/update_name",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateTeamName", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_teams_update_name');
|
||||
};
|
||||
|
||||
module.exports.signupTeam = function(email, name, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/signup",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({email: email, name: name}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("singupTeam", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_teams_signup');
|
||||
};
|
||||
|
||||
module.exports.createTeam = function(team, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/create",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(team),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("createTeam", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.findTeamByDomain = function(domain, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/find_team_by_domain",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({domain: domain}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("findTeamByDomain", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.findTeamsSendEmail = function(email, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/email_teams",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({email: email}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("findTeamsSendEmail", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_teams_email_teams');
|
||||
};
|
||||
|
||||
module.exports.findTeams = function(email, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/teams/find_teams",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({email: email}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("findTeams", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createChannel = function(channel, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/create",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(channel),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("createChannel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_create', channel.type, 'name', channel.name);
|
||||
};
|
||||
|
||||
module.exports.updateChannel = function(channel, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/update",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(channel),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateChannel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_update');
|
||||
};
|
||||
|
||||
module.exports.updateChannelDesc = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/update_desc",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateChannelDesc", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_desc');
|
||||
};
|
||||
|
||||
module.exports.updateNotifyLevel = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/update_notify_level",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateNotifyLevel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.joinChannel = function(id, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/join",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("joinChannel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_join');
|
||||
};
|
||||
|
||||
module.exports.leaveChannel = function(id, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/leave",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("leaveChannel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_leave');
|
||||
};
|
||||
|
||||
module.exports.deleteChannel = function(id, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/delete",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("deleteChannel", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_delete');
|
||||
};
|
||||
|
||||
module.exports.updateLastViewedAt = function(channelId, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + channelId + "/update_last_viewed_at",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updateLastViewedAt", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getChannels = function(success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/",
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
ifModified: true,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getChannels", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getMoreChannels = function(success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/more",
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
ifModified: true,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getMoreChannels", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getChannelExtraInfo = function(id, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/extra_info",
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getChannelExtraInfo", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.executeCommand = function(channelId, command, suggest, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/command",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({channelId: channelId, command: command, suggest: "" + suggest}),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("executeCommand", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getPosts = function(channelId, offset, limit, success, error, complete) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + channelId + "/posts/" + offset + "/" + limit,
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
ifModified: true,
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
try {
|
||||
e = handleError("getPosts", xhr, status, err);
|
||||
error(e);
|
||||
} catch(er) {
|
||||
console.error(er);
|
||||
}
|
||||
},
|
||||
complete: complete
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getPost = function(channelId, postId, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + channelId + "/post/" + postId,
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
ifModified: false,
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getPost", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.search = function(terms, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/posts/search",
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
data: {"terms": terms},
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("search", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_posts_search');
|
||||
};
|
||||
|
||||
module.exports.deletePost = function(channelId, id, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + channelId + "/post/" + id + "/delete",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("deletePost", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_posts_delete');
|
||||
};
|
||||
|
||||
module.exports.createPost = function(post, channel, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/"+ post.channel_id + "/create",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(post),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("createPost", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_posts_create', channel.name, 'length', post.message.length);
|
||||
|
||||
// global.window.analytics.track('api_posts_create', {
|
||||
// category: 'api',
|
||||
// channel_name: channel.name,
|
||||
// channel_type: channel.type,
|
||||
// length: post.message.length,
|
||||
// files: (post.filenames || []).length,
|
||||
// mentions: (post.message.match("/<mention>/g") || []).length
|
||||
// });
|
||||
};
|
||||
|
||||
module.exports.updatePost = function(post, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/"+ post.channel_id + "/update",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(post),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("updatePost", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_posts_update');
|
||||
};
|
||||
|
||||
module.exports.addChannelMember = function(id, data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/add",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("addChannelMember", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_add_member');
|
||||
};
|
||||
|
||||
module.exports.removeChannelMember = function(id, data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/channels/" + id + "/remove",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("removeChannelMember", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_channels_remove_member');
|
||||
};
|
||||
|
||||
module.exports.getProfiles = function(success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/profiles",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
ifModified: true,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getProfiles", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.uploadFile = function(formData, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/files/upload",
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("uploadFile", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_files_upload');
|
||||
};
|
||||
|
||||
module.exports.getPublicLink = function(data, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/files/get_public_link",
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getPublicLink", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.uploadProfileImage = function(imageData, success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/newimage",
|
||||
type: 'POST',
|
||||
data: imageData,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("uploadProfileImage", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getStatuses = function(success, error) {
|
||||
$.ajax({
|
||||
url: "/api/v1/users/status",
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
error: function(xhr, status, err) {
|
||||
e = handleError("getStatuses", xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
78
web/react/utils/constants.jsx
Обычный файл
78
web/react/utils/constants.jsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var keyMirror = require('keymirror');
|
||||
|
||||
module.exports = {
|
||||
ActionTypes: keyMirror({
|
||||
RECIEVED_ERROR: null,
|
||||
|
||||
CLICK_CHANNEL: null,
|
||||
CREATE_CHANNEL: null,
|
||||
RECIEVED_CHANNELS: null,
|
||||
RECIEVED_MORE_CHANNELS: null,
|
||||
RECIEVED_CHANNEL_EXTRA_INFO: null,
|
||||
|
||||
RECIEVED_POSTS: null,
|
||||
RECIEVED_SEARCH: null,
|
||||
RECIEVED_POST_SELECTED: null,
|
||||
RECIEVED_MENTION_DATA: null,
|
||||
RECIEVED_ADD_MENTION: null,
|
||||
|
||||
RECIEVED_PROFILES: null,
|
||||
RECIEVED_ME: null,
|
||||
RECIEVED_SESSIONS: null,
|
||||
RECIEVED_AUDITS: null,
|
||||
RECIEVED_TEAMS: null,
|
||||
RECIEVED_STATUSES: null,
|
||||
|
||||
RECIEVED_MSG: null,
|
||||
}),
|
||||
|
||||
PayloadSources: keyMirror({
|
||||
SERVER_ACTION: null,
|
||||
VIEW_ACTION: null
|
||||
}),
|
||||
CHARACTER_LIMIT: 4000,
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
|
||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
|
||||
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
|
||||
SPREADSHEET_TYPES: ['ppt', 'pptx', 'csv'],
|
||||
EXCEL_TYPES: ['xlsx'],
|
||||
WORD_TYPES: ['doc', 'docx'],
|
||||
CODE_TYPES: ['css', 'html', 'js', 'php', 'rb'],
|
||||
PDF_TYPES: ['pdf'],
|
||||
PATCH_TYPES: ['patch'],
|
||||
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
|
||||
MAX_DISPLAY_FILES: 5,
|
||||
MAX_FILE_SIZE: 50000000, // 50 MB
|
||||
DEFAULT_CHANNEL: 'town-square',
|
||||
POST_CHUNK_SIZE: 60,
|
||||
RESERVED_DOMAINS: [
|
||||
"www",
|
||||
"web",
|
||||
"admin",
|
||||
"support",
|
||||
"notify",
|
||||
"test",
|
||||
"demo",
|
||||
"mail",
|
||||
"team",
|
||||
"channel",
|
||||
"internal",
|
||||
"localhost",
|
||||
"stag",
|
||||
"post",
|
||||
"cluster",
|
||||
"api",
|
||||
],
|
||||
RESERVED_USERNAMES: [
|
||||
"valet",
|
||||
"all",
|
||||
"channel",
|
||||
],
|
||||
MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
MAX_DMS: 10,
|
||||
ONLINE_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 class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
|
||||
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>"
|
||||
};
|
||||
732
web/react/utils/utils.jsx
Обычный файл
732
web/react/utils/utils.jsx
Обычный файл
@@ -0,0 +1,732 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
var AsyncClient = require('./async_client.jsx');
|
||||
var client = require('./client.jsx');
|
||||
var LinkifyIt = require('linkify-it');
|
||||
|
||||
module.exports.isEmail = function(email) {
|
||||
var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
|
||||
return regex.test(email);
|
||||
};
|
||||
|
||||
module.exports.cleanUpUrlable = function(input) {
|
||||
var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
|
||||
cleaned = cleaned.replace(/^\-+/, '');
|
||||
cleaned = cleaned.replace(/\-+$/, '');
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.isTestDomain = function() {
|
||||
|
||||
if ((/^localhost/).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
if ((/^test/).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
if ((/^127.0./).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
if ((/^192.168./).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
if ((/^10./).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
if ((/^176./).test(window.location.hostname))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
var getSubDomain = function() {
|
||||
|
||||
if (module.exports.isTestDomain())
|
||||
return "";
|
||||
|
||||
if ((/^www/).test(window.location.hostname))
|
||||
return "";
|
||||
|
||||
if ((/^beta/).test(window.location.hostname))
|
||||
return "";
|
||||
|
||||
if ((/^ci/).test(window.location.hostname))
|
||||
return "";
|
||||
|
||||
var parts = window.location.hostname.split(".");
|
||||
|
||||
if (parts.length != 3)
|
||||
return "";
|
||||
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
global.window.getSubDomain = getSubDomain;
|
||||
module.exports.getSubDomain = getSubDomain;
|
||||
|
||||
module.exports.getDomainWithOutSub = function() {
|
||||
|
||||
var parts = window.location.host.split(".");
|
||||
|
||||
if (parts.length == 1)
|
||||
return "localhost:8065";
|
||||
|
||||
return parts[1] + "." + parts[2];
|
||||
}
|
||||
|
||||
module.exports.getCookie = function(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
|
||||
module.exports.notifyMe = function(title, body, channel) {
|
||||
if ("Notification" in window && Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(function (permission) {
|
||||
if (Notification.permission !== permission) {
|
||||
Notification.permission = permission;
|
||||
}
|
||||
|
||||
if (permission === "granted") {
|
||||
var notification = new Notification(title,
|
||||
{ body: body, tag: body, icon: '/static/images/icon50x50.gif' }
|
||||
);
|
||||
notification.onclick = function() {
|
||||
window.focus();
|
||||
if (channel) {
|
||||
module.exports.switchChannel(channel);
|
||||
} else {
|
||||
window.location.href = "/channels/town-square";
|
||||
}
|
||||
};
|
||||
setTimeout(function(){
|
||||
notification.close();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ding = function() {
|
||||
var audio = new Audio('/static/images/ding.mp3');
|
||||
audio.play();
|
||||
}
|
||||
|
||||
module.exports.getUrlParameter = function(sParam) {
|
||||
var sPageURL = window.location.search.substring(1);
|
||||
var sURLVariables = sPageURL.split('&');
|
||||
for (var i = 0; i < sURLVariables.length; i++)
|
||||
{
|
||||
var sParameterName = sURLVariables[i].split('=');
|
||||
if (sParameterName[0] == sParam)
|
||||
{
|
||||
return sParameterName[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports.getDateForUnixTicks = function(ticks) {
|
||||
return new Date(ticks)
|
||||
}
|
||||
|
||||
module.exports.displayDate = function(ticks) {
|
||||
var d = new Date(ticks);
|
||||
var m_names = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");
|
||||
|
||||
return m_names[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
|
||||
}
|
||||
|
||||
module.exports.displayTime = function(ticks) {
|
||||
var d = new Date(ticks);
|
||||
var hours = d.getHours();
|
||||
var minutes = d.getMinutes();
|
||||
var ampm = hours >= 12 ? "PM" : "AM";
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : "12"
|
||||
minutes = minutes > 9 ? minutes : '0'+minutes
|
||||
return hours + ":" + minutes + " " + ampm
|
||||
}
|
||||
|
||||
module.exports.displayDateTime = function(ticks) {
|
||||
var seconds = Math.floor((Date.now() - ticks) / 1000)
|
||||
|
||||
interval = Math.floor(seconds / 3600);
|
||||
|
||||
if (interval > 24) {
|
||||
return this.displayTime(ticks)
|
||||
}
|
||||
|
||||
if (interval > 1) {
|
||||
return interval + " hours ago";
|
||||
}
|
||||
|
||||
if (interval == 1) {
|
||||
return interval + " hour ago";
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1) {
|
||||
return interval + " minutes ago";
|
||||
}
|
||||
|
||||
return "1 minute ago";
|
||||
|
||||
}
|
||||
|
||||
// returns Unix timestamp in milliseconds
|
||||
module.exports.getTimestamp = function() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
module.exports.extractLinks = function(text) {
|
||||
var repRegex = new RegExp("<br>", "g");
|
||||
var linkMatcher = new LinkifyIt();
|
||||
var matches = linkMatcher.match(text.replace(repRegex, "\n"));
|
||||
|
||||
if (!matches) return { "links": null, "text": text };
|
||||
|
||||
var links = []
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
links.push(matches[i].url)
|
||||
}
|
||||
|
||||
return { "links": links, "text": text };
|
||||
}
|
||||
|
||||
module.exports.escapeRegExp = function(string) {
|
||||
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
}
|
||||
|
||||
module.exports.getEmbed = function(link) {
|
||||
|
||||
var ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
|
||||
|
||||
var match = link.trim().match(ytRegex);
|
||||
if (match && match[1].length==11){
|
||||
return getYoutubeEmbed(link);
|
||||
}
|
||||
|
||||
// Generl embed feature turned off for now
|
||||
return;
|
||||
|
||||
var id = parseInt((Math.random() * 1000000) + 1);
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: "https://query.yahooapis.com/v1/public/yql",
|
||||
data: {
|
||||
q: "select * from html where url=\""+link+"\" and xpath='html/head'",
|
||||
format: "json"
|
||||
},
|
||||
async: true
|
||||
}).done(function(data) {
|
||||
if(!data.query.results) {
|
||||
return;
|
||||
}
|
||||
|
||||
var headerData = data.query.results.head;
|
||||
|
||||
var description = ""
|
||||
for(var i = 0; i < headerData.meta.length; i++) {
|
||||
if(headerData.meta[i].name && (headerData.meta[i].name === "description" || headerData.meta[i].name === "Description")){
|
||||
description = headerData.meta[i].content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$('.embed-title.'+id).html(headerData.title);
|
||||
$('.embed-description.'+id).html(description);
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="post-comment">
|
||||
<div className={"web-embed-data"}>
|
||||
<p className={"embed-title " + id} />
|
||||
<p className={"embed-description " + id} />
|
||||
<p className={"embed-link " + id}>{link}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var getYoutubeEmbed = function(link) {
|
||||
var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
|
||||
|
||||
var youtubeId = link.trim().match(regex)[1];
|
||||
|
||||
var onclick = function(e) {
|
||||
var div = $(e.target).closest('.video-thumbnail__container')[0];
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("src",
|
||||
"https://www.youtube.com/embed/" + div.id
|
||||
+ "?autoplay=1&autohide=1&border=0&wmode=opaque&enablejsapi=1");
|
||||
iframe.setAttribute("width", "480px");
|
||||
iframe.setAttribute("height", "360px");
|
||||
iframe.setAttribute("type", "text/html");
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
|
||||
div.parentNode.replaceChild(iframe, div);
|
||||
};
|
||||
|
||||
var success = function(data) {
|
||||
$('.video-uploader.'+youtubeId).html(data.data.uploader);
|
||||
$('.video-title.'+youtubeId).find('a').html(data.data.title);
|
||||
$(".post-list-holder-by-time").scrollTop($(".post-list-holder-by-time")[0].scrollHeight);
|
||||
$(".post-list-holder-by-time").perfectScrollbar('update');
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
async: true,
|
||||
url: 'https://gdata.youtube.com/feeds/api/videos/'+youtubeId+'?v=2&alt=jsonc',
|
||||
type: 'GET',
|
||||
success: success
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="post-comment">
|
||||
<h4 className="video-type">YouTube</h4>
|
||||
<h4 className={"video-uploader "+youtubeId}></h4>
|
||||
<h4 className={"video-title "+youtubeId}><a href={link}></a></h4>
|
||||
<div className="video-div embed-responsive-item" id={youtubeId} onClick={onclick}>
|
||||
<div className="embed-responsive embed-responsive-4by3 video-div__placeholder">
|
||||
<div id={youtubeId} className="video-thumbnail__container">
|
||||
<img className="video-thumbnail" src={"https://i.ytimg.com/vi/" + youtubeId + "/hqdefault.jpg"}/>
|
||||
<div className="block">
|
||||
<span className="play-button"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
module.exports.areStatesEqual = function(state1, state2) {
|
||||
return JSON.stringify(state1) === JSON.stringify(state2);
|
||||
}
|
||||
|
||||
module.exports.replaceHtmlEntities = function(text) {
|
||||
var tagsToReplace = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
for (var tag in tagsToReplace) {
|
||||
var regex = new RegExp(tag, "g");
|
||||
text = text.replace(regex, tagsToReplace[tag]);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
module.exports.insertHtmlEntities = function(text) {
|
||||
var tagsToReplace = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
for (var tag in tagsToReplace) {
|
||||
var regex = new RegExp(tag, "g");
|
||||
text = text.replace(regex, tagsToReplace[tag]);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
module.exports.searchForTerm = function(term) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH_TERM,
|
||||
term: term,
|
||||
do_search: true
|
||||
});
|
||||
}
|
||||
|
||||
var oldExplicitMentionRegex = /(?:<mention>)([\s\S]*?)(?:<\/mention>)/g;
|
||||
var puncStartRegex = /^((?![@#])\W)+/g;
|
||||
var puncEndRegex = /(\W)+$/g;
|
||||
|
||||
module.exports.textToJsx = function(text, options) {
|
||||
|
||||
if (options && options['singleline']) {
|
||||
var repRegex = new RegExp("\n", "g");
|
||||
text = text.replace(repRegex, " ");
|
||||
} else {
|
||||
var repRegex = new RegExp("\n", "g");
|
||||
text = text.replace(repRegex, "<br>");
|
||||
}
|
||||
|
||||
var searchTerm = ""
|
||||
if (options && options['searchTerm']) {
|
||||
searchTerm = options['searchTerm'].toLowerCase()
|
||||
}
|
||||
|
||||
var mentionClass = "mention-highlight";
|
||||
if (options && options['noMentionHighlight']) {
|
||||
mentionClass = "";
|
||||
}
|
||||
|
||||
var inner = [];
|
||||
|
||||
// Function specific regexes
|
||||
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$/g;
|
||||
|
||||
var implicitKeywords = {};
|
||||
var keywordArray = UserStore.getCurrentMentionKeys();
|
||||
for (var i = 0; i < keywordArray.length; i++) {
|
||||
implicitKeywords[keywordArray[i]] = true;
|
||||
}
|
||||
|
||||
var lines = text.split("<br>");
|
||||
var urlMatcher = new LinkifyIt();
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
var words = line.split(" ");
|
||||
var highlightSearchClass = "";
|
||||
for (var z = 0; z < words.length; z++) {
|
||||
var word = words[z];
|
||||
var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
|
||||
var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
|
||||
var explicitMention = mentionRegex.exec(trimWord);
|
||||
|
||||
if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != "") {
|
||||
|
||||
highlightSearchClass = " search-highlight";
|
||||
}
|
||||
|
||||
if (explicitMention && UserStore.getProfileByUsername(explicitMention[1])) {
|
||||
var name = explicitMention[1];
|
||||
// do both a non-case sensitive and case senstive check
|
||||
var mClass = (name.toLowerCase() in implicitKeywords || name in implicitKeywords) ? mentionClass : "";
|
||||
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
|
||||
if (searchTerm === name) {
|
||||
highlightSearchClass = " search-highlight";
|
||||
}
|
||||
|
||||
inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function() {module.exports.searchForTerm(name);}}>@{name}</a>{suffix} </span>);
|
||||
} else if (urlMatcher.test(word)) {
|
||||
var match = urlMatcher.match(word)[0];
|
||||
var link = match.url;
|
||||
|
||||
var prefix = word.substring(0,word.indexOf(match.raw))
|
||||
var suffix = word.substring(word.indexOf(match.raw)+match.raw.length);
|
||||
|
||||
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_link"} className={"theme" + highlightSearchClass} target="_blank" href={link}>{match.raw}</a>{suffix} </span>);
|
||||
|
||||
} else if (trimWord.match(hashRegex)) {
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
var mClass = trimWord in implicitKeywords ? mentionClass : "";
|
||||
|
||||
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
|
||||
highlightSearchClass = " search-highlight";
|
||||
}
|
||||
|
||||
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
|
||||
|
||||
} else if (trimWord in implicitKeywords) {
|
||||
var suffix = word.match(puncEndRegex);
|
||||
var prefix = word.match(puncStartRegex);
|
||||
|
||||
if (trimWord.charAt(0) === '@') {
|
||||
if (searchTerm === trimWord.substring(1).toLowerCase()) {
|
||||
highlightSearchClass = " search-highlight";
|
||||
}
|
||||
inner.push(<span key={word+i+z+"_span"} key={name+i+z+"_span"}>{prefix}<a className={mentionClass + highlightSearchClass} key={name+i+z+"_link"} href="#">{trimWord}</a>{suffix} </span>);
|
||||
} else {
|
||||
inner.push(<span key={word+i+z+"_span"}>{prefix}<span className={mentionClass + highlightSearchClass}>{module.exports.replaceHtmlEntities(trimWord)}</span>{suffix} </span>);
|
||||
}
|
||||
|
||||
} else if (word === "") {
|
||||
// if word is empty dont include a span
|
||||
} else {
|
||||
inner.push(<span key={word+i+z+"_span"}><span className={highlightSearchClass}>{module.exports.replaceHtmlEntities(word)}</span> </span>);
|
||||
}
|
||||
highlightSearchClass = "";
|
||||
}
|
||||
if (i != lines.length-1)
|
||||
inner.push(<br key={"br_"+i+z}/>);
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
module.exports.getFileType = function(ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (Constants.IMAGE_TYPES.indexOf(ext) > -1) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
if (Constants.AUDIO_TYPES.indexOf(ext) > -1) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
if (Constants.VIDEO_TYPES.indexOf(ext) > -1) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
if (Constants.SPREADSHEET_TYPES.indexOf(ext) > -1) {
|
||||
return "spreadsheet";
|
||||
}
|
||||
|
||||
if (Constants.CODE_TYPES.indexOf(ext) > -1) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
if (Constants.WORD_TYPES.indexOf(ext) > -1) {
|
||||
return "word";
|
||||
}
|
||||
|
||||
if (Constants.EXCEL_TYPES.indexOf(ext) > -1) {
|
||||
return "excel";
|
||||
}
|
||||
|
||||
if (Constants.PDF_TYPES.indexOf(ext) > -1) {
|
||||
return "pdf";
|
||||
}
|
||||
|
||||
if (Constants.PATCH_TYPES.indexOf(ext) > -1) {
|
||||
return "patch";
|
||||
}
|
||||
|
||||
return "other";
|
||||
};
|
||||
|
||||
module.exports.getIconClassName = function(fileType) {
|
||||
fileType = fileType.toLowerCase();
|
||||
|
||||
if (fileType in Constants.ICON_FROM_TYPE)
|
||||
return Constants.ICON_FROM_TYPE[fileType];
|
||||
|
||||
return "glyphicon-file";
|
||||
}
|
||||
|
||||
module.exports.splitFileLocation = function(fileLocation) {
|
||||
var fileSplit = fileLocation.split('.');
|
||||
if (fileSplit.length < 2) return {};
|
||||
|
||||
var ext = fileSplit[fileSplit.length-1];
|
||||
fileSplit.splice(fileSplit.length-1,1)
|
||||
var filePath = fileSplit.join('.');
|
||||
var filename = filePath.split('/')[filePath.split('/').length-1];
|
||||
|
||||
return {'ext': ext, 'name': filename, 'path': filePath};
|
||||
}
|
||||
|
||||
module.exports.toTitleCase = function(str) {
|
||||
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
||||
}
|
||||
|
||||
module.exports.changeCss = function(className, classValue) {
|
||||
// we need invisible container to store additional css definitions
|
||||
var cssMainContainer = $('#css-modifier-container');
|
||||
if (cssMainContainer.length == 0) {
|
||||
var cssMainContainer = $('<div id="css-modifier-container"></div>');
|
||||
cssMainContainer.hide();
|
||||
cssMainContainer.appendTo($('body'));
|
||||
}
|
||||
|
||||
// and we need one div for each class
|
||||
classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
|
||||
if (classContainer.length == 0) {
|
||||
classContainer = $('<div data-class="' + className + '"></div>');
|
||||
classContainer.appendTo(cssMainContainer);
|
||||
}
|
||||
|
||||
// append additional style
|
||||
classContainer.html('<style>' + className + ' {' + classValue + '}</style>');
|
||||
}
|
||||
|
||||
module.exports.rgb2hex = function(rgb) {
|
||||
if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb;
|
||||
|
||||
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||
function hex(x) {
|
||||
return ("0" + parseInt(x).toString(16)).slice(-2);
|
||||
}
|
||||
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
|
||||
}
|
||||
|
||||
module.exports.placeCaretAtEnd = function(el) {
|
||||
el.focus();
|
||||
if (typeof window.getSelection != "undefined"
|
||||
&& typeof document.createRange != "undefined") {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
range.collapse(false);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
} else if (typeof document.body.createTextRange != "undefined") {
|
||||
var textRange = document.body.createTextRange();
|
||||
textRange.moveToElementText(el);
|
||||
textRange.collapse(false);
|
||||
textRange.select();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getCaretPosition = function(el) {
|
||||
if (el.selectionStart) {
|
||||
return el.selectionStart;
|
||||
} else if (document.selection) {
|
||||
el.focus();
|
||||
|
||||
var r = document.selection.createRange();
|
||||
if (r == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var re = el.createTextRange(),
|
||||
rc = re.duplicate();
|
||||
re.moveToBookmark(r.getBookmark());
|
||||
rc.setEndPoint('EndToStart', re);
|
||||
|
||||
return rc.text.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
module.exports.setSelectionRange = function(input, selectionStart, selectionEnd) {
|
||||
if (input.setSelectionRange) {
|
||||
input.focus();
|
||||
input.setSelectionRange(selectionStart, selectionEnd);
|
||||
}
|
||||
else if (input.createTextRange) {
|
||||
var range = input.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', selectionEnd);
|
||||
range.moveStart('character', selectionStart);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.setCaretPosition = function (input, pos) {
|
||||
module.exports.setSelectionRange(input, pos, pos);
|
||||
}
|
||||
|
||||
module.exports.getSelectedText = function(input) {
|
||||
var selectedText;
|
||||
if (document.selection != undefined) {
|
||||
input.focus();
|
||||
var sel = document.selection.createRange();
|
||||
selectedText = sel.text;
|
||||
} else if (input.selectionStart != undefined) {
|
||||
var startPos = input.selectionStart;
|
||||
var endPos = input.selectionEnd;
|
||||
selectedText = input.value.substring(startPos, endPos)
|
||||
}
|
||||
|
||||
return selectedText;
|
||||
}
|
||||
|
||||
module.exports.isValidUsername = function (name) {
|
||||
|
||||
var error = ""
|
||||
if (!name) {
|
||||
error = "This field is required";
|
||||
}
|
||||
|
||||
else if (name.length < 3 || name.length > 15)
|
||||
{
|
||||
error = "Must be between 3 and 15 characters";
|
||||
}
|
||||
|
||||
else if (!/^[a-z0-9\.\-\_]+$/.test(name))
|
||||
{
|
||||
error = "Must contain only lowercase letters, numbers, and the symbols '.', '-', and '_'.";
|
||||
}
|
||||
|
||||
else if (!/[a-z]/.test(name.charAt(0)))
|
||||
{
|
||||
error = "First character must be a letter.";
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var lowerName = name.toLowerCase().trim();
|
||||
|
||||
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++)
|
||||
{
|
||||
if (lowerName === Constants.RESERVED_USERNAMES[i])
|
||||
{
|
||||
error = "Cannot use a reserved word as a username.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
module.exports.switchChannel = function(channel, teammate_name) {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.CLICK_CHANNEL,
|
||||
name: channel.name,
|
||||
id: channel.id
|
||||
});
|
||||
|
||||
var domain = window.location.href.split('/channels')[0];
|
||||
history.replaceState('data', '', domain + '/channels/' + channel.name);
|
||||
|
||||
if (channel.type === 'D' && teammate_name) {
|
||||
document.title = teammate_name + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
} else {
|
||||
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"));
|
||||
}
|
||||
|
||||
AsyncClient.getChannels(true, true, true);
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
AsyncClient.getPosts(true, channel.id);
|
||||
|
||||
$('.inner__wrap').removeClass('move--right');
|
||||
$('.sidebar--left').removeClass('move--right');
|
||||
|
||||
client.trackPage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports.isMobile = function() {
|
||||
return screen.width <= 768;
|
||||
}
|
||||
|
||||
module.exports.isComment = function(post) {
|
||||
if ('root_id' in post) {
|
||||
return post.root_id != "";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Image.prototype.load = function(url, progressCallback) {
|
||||
var thisImg = this;
|
||||
var xmlHTTP = new XMLHttpRequest();
|
||||
xmlHTTP.open('GET', url, true);
|
||||
xmlHTTP.responseType = 'arraybuffer';
|
||||
xmlHTTP.onload = function(e) {
|
||||
var h = xmlHTTP.getAllResponseHeaders(),
|
||||
m = h.match( /^Content-Type\:\s*(.*?)$/mi ),
|
||||
mimeType = m[ 1 ] || 'image/png';
|
||||
|
||||
var blob = new Blob([this.response], { type: mimeType });
|
||||
thisImg.src = window.URL.createObjectURL(blob);
|
||||
};
|
||||
xmlHTTP.onprogress = function(e) {
|
||||
parseInt(thisImg.completedPercentage = (e.loaded / e.total) * 100);
|
||||
if (progressCallback) progressCallback();
|
||||
};
|
||||
xmlHTTP.onloadstart = function() {
|
||||
thisImg.completedPercentage = 0;
|
||||
};
|
||||
xmlHTTP.send();
|
||||
};
|
||||
|
||||
Image.prototype.completedPercentage = 0;
|
||||
Ссылка в новой задаче
Block a user