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>
|
||||
);
|
||||
}
|
||||
});
|
||||
Ссылка в новой задаче
Block a user