Этот коммит содержится в:
=Corey Hulen
2015-06-14 23:53:32 -08:00
Коммит 56e74239d6
646 изменённых файлов: 127767 добавлений и 0 удалений

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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

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

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

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

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

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

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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>
);
}
});

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

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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">&times;</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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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>&nbsp;<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>&nbsp;
<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>&nbsp;
<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 Обычный файл
Просмотреть файл

@@ -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 } Youll 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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -0,0 +1,189 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx');
module.exports = React.createClass({
handleNext: function() {
var id = this.state.imgId + 1;
if (id > this.props.filenames.length-1) {
id = 0;
}
this.setState({ imgId: id });
this.loadImage(id);
},
handlePrev: function() {
var id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length-1;
}
this.setState({ imgId: id });
this.loadImage(id);
},
componentWillReceiveProps: function(nextProps) {
this.setState({ imgId: nextProps.startId });
},
loadImage: function(id) {
if (this.state.loaded[id] || this.state.images[id]) return;
var src = "";
if (this.props.imgCount > 0) {
src = this.props.filenames[id];
} else {
var fileInfo = utils.splitFileLocation(this.props.filenames[id]);
src = fileInfo['path'] + '_preview.jpg';
}
var self = this;
var img = new Image();
img.load(src,
function(){
var progress = self.state.progress;
progress[id] = img.completedPercentage;
self.setState({ progress: progress });
});
img.onload = function(imgid) {
return function() {
var loaded = self.state.loaded;
loaded[imgid] = true;
self.setState({ loaded: loaded });
};
}(id);
var images = this.state.images;
images[id] = img;
this.setState({ images: images });
},
componentDidUpdate: function() {
if (this.refs.image) {
var height = $(window).height()-100;
if (this.state.loaded[this.state.imgId]) {
$(this.refs.imageWrap.getDOMNode()).removeClass("default");
$(this.refs.image.getDOMNode()).css("max-height",height);
}
}
},
componentDidMount: function() {
var self = this;
$("#"+this.props.modalId).on('shown.bs.modal', function() {
self.setState({ viewed: true });
self.loadImage(self.state.imgId);
})
$(this.refs.modal.getDOMNode()).click(function(e){
if (e.target == this || e.target == self.refs.imageBody.getDOMNode()) {
$('.image_modal').modal('hide');
}
});
$(this.refs.imageWrap.getDOMNode()).hover(
function() {
$(self.refs.imageFooter.getDOMNode()).addClass("footer--show");
}, function() {
$(self.refs.imageFooter.getDOMNode()).removeClass("footer--show");
}
);
},
getPublicLink: function(e) {
data = {};
data["channel_id"] = this.props.channelId;
data["user_id"] = this.props.userId;
data["filename"] = this.props.filenames[this.state.imgId];
Client.getPublicLink(data,
function(data) {
window.open(data["public_link"]);
}.bind(this),
function(err) {
}.bind(this)
);
},
getInitialState: function() {
var loaded = [];
var progress = [];
for (var i = 0; i < this.props.filenames.length; i ++) {
loaded.push(false);
progress.push(0);
}
return { imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {} };
},
render: function() {
if (this.props.filenames.length < 1 || this.props.filenames.length-1 < this.state.imgId) {
return <div/>;
}
var fileInfo = utils.splitFileLocation(this.props.filenames[this.state.imgId]);
var name = fileInfo['name'] + '.' + fileInfo['ext'];
var loading = "";
var bgClass = "";
var img = {};
if (!this.state.loaded[this.state.imgId]) {
var percentage = Math.floor(this.state.progress[this.state.imgId]);
loading = (
<div key={name+"_loading"}>
<img ref="placeholder" className="loader-image" src="/static/images/load.gif" />
{ percentage > 0 ?
<span className="loader-percent" >{"Downloading " + percentage + "%"}</span>
: ""}
</div>
);
bgClass = "black-bg";
} else if (this.state.viewed) {
for (var id in this.state.images) {
var info = utils.splitFileLocation(this.props.filenames[id]);
var preview_filename = "";
if (this.props.imgCount > 0) {
preview_filename = this.props.filenames[this.state.imgId];
} else {
preview_filename = info['path'] + '_preview.jpg';
}
var imgClass = "hidden";
if (this.state.loaded[id] && this.state.imgId == id) imgClass = "";
img[info['path']] = <a key={info['path']} className={imgClass} href={this.props.filenames[id]} target="_blank"><img ref="image" src={preview_filename}/></a>;
}
}
var imgFragment = React.addons.createFragment(img);
return (
<div className="modal fade image_modal" ref="modal" id={this.props.modalId} tabIndex="-1" role="dialog" aria-hidden="true">
<div className="modal-dialog modal-image">
<div className="modal-content image-content">
<div ref="imageBody" className="modal-body image-body">
<div ref="imageWrap" className={"image-wrapper default " + bgClass}>
<div className="modal-close" data-dismiss="modal"></div>
{imgFragment}
<div ref="imageFooter" className="modal-button-bar">
<span className="pull-left text">{"Image "+(this.state.imgId+1)+" of "+this.props.filenames.length}</span>
<div className="image-links">
{ config.AllowPublicLink ?
<div>
<a href="#" className="text" data-title="Public Image" onClick={this.getPublicLink}>Get Public Link</a>
<span className="text"> | </span>
</div>
: "" }
<a href={this.props.filenames[id]} download={name} className="text">Download</a>
</div>
</div>
{loading}
</div>
{ this.props.filenames.length > 1 ?
<a className="modal-prev-bar" href="#" onClick={this.handlePrev}>
<i className="image-control image-prev"/>
</a>
: "" }
{ this.props.filenames.length > 1 ?
<a className="modal-next-bar" href="#" onClick={this.handleNext}>
<i className="image-control image-next"/>
</a>
: "" }
</div>
</div>
</div>
</div>
);
}
});

30
web/react/dispatcher/app_dispatcher.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Dispatcher = require('flux').Dispatcher;
var assign = require('object-assign');
var Constants = require('../utils/constants.jsx');
var PayloadSources = Constants.PayloadSources;
var AppDispatcher = assign(new Dispatcher(), {
handleServerAction: function(action) {
var payload = {
source: PayloadSources.SERVER_ACTION,
action: action
};
this.dispatch(payload);
},
handleViewAction: function(action) {
var payload = {
source: PayloadSources.VIEW_ACTION,
action: action
};
this.dispatch(payload);
}
});
module.exports = AppDispatcher;

37
web/react/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,37 @@
{
"name": "mattermost",
"version": "0.0.1",
"private": true,
"dependencies": {
"flux": "^2.0.0",
"keymirror": "~0.1.0",
"object-assign": "^1.0.0",
"react": "^0.12.0",
"autolinker": "^0.15.2",
"linkify-it": "^1.1.0",
"react-zeroclipboard-mixin": "^0.1.0"
},
"devDependencies": {
"browserify": "^6.2.0",
"envify": "^3.0.0",
"jest-cli": "~0.1.17",
"reactify": "^0.15.2",
"uglify-js": "~2.4.15",
"watchify": "^2.1.1"
},
"scripts": {
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",
"build": "NODE_ENV=production browserify ./**/*.jsx | uglifyjs > ../static/js/bundle.min.js",
"build_old": "NODE_ENV=production browserify ./**/*.jsx | uglifyjs -cm > ../static/js/bundle.min.js",
"test": "jest"
},
"browserify": {
"transform": [
"reactify",
"envify"
]
},
"jest": {
"rootDir": "."
}
}

197
web/react/pages/channel.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,197 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Navbar = require('../components/navbar.jsx');
var Sidebar = require('../components/sidebar.jsx');
var ChannelHeader = require('../components/channel_header.jsx');
var PostList = require('../components/post_list.jsx');
var CreatePost = require('../components/create_post.jsx');
var SidebarRight = require('../components/sidebar_right.jsx');
var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
var GetLinkModal = require('../components/get_link_modal.jsx');
var MemberInviteModal = require('../components/invite_member_modal.jsx');
var EditChannelModal = require('../components/edit_channel_modal.jsx');
var DeleteChannelModal = require('../components/delete_channel_modal.jsx');
var RenameChannelModal = require('../components/rename_channel_modal.jsx');
var RenameTeamModal = require('../components/rename_team_modal.jsx');
var EditPostModal = require('../components/edit_post_modal.jsx');
var DeletePostModal = require('../components/delete_post_modal.jsx');
var MoreChannelsModal = require('../components/more_channels.jsx');
var NewChannelModal = require('../components/new_channel.jsx');
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
var UserSettingsModal = require('../components/settings_modal.jsx');
var ChannelMembersModal = require('../components/channel_members.jsx');
var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
var DirectChannelModal = require('../components/more_direct_channels.jsx');
var ErrorBar = require('../components/error_bar.jsx')
var ChannelLoader = require('../components/channel_loader.jsx');
var MentionList = require('../components/mention_list.jsx');
var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
global.window.setup_channel_page = function(team_name, team_type, channel_name, channel_id) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: channel_name,
id: channel_id
});
React.render(
<ErrorBar/>,
document.getElementById('error_bar')
);
React.render(
<ChannelLoader/>,
document.getElementById('channel_loader')
);
React.render(
<Navbar teamName={team_name} />,
document.getElementById('navbar')
);
React.render(
<Sidebar teamName={team_name} teamType={team_type} />,
document.getElementById('sidebar-left')
);
React.render(
<RenameTeamModal teamName={team_name} />,
document.getElementById('rename_team_modal')
);
React.render(
<GetLinkModal />,
document.getElementById('get_link_modal')
);
React.render(
<UserSettingsModal />,
document.getElementById('user_settings_modal')
);
React.render(
<TeamMembersModal teamName={team_name} />,
document.getElementById('team_members_modal')
);
React.render(
<MemberInviteModal />,
document.getElementById('invite_member_modal')
);
React.render(
<ChannelHeader />,
document.getElementById('channel-header')
);
React.render(
<EditChannelModal />,
document.getElementById('edit_channel_modal')
);
React.render(
<DeleteChannelModal />,
document.getElementById('delete_channel_modal')
);
React.render(
<RenameChannelModal />,
document.getElementById('rename_channel_modal')
);
React.render(
<ChannelNotificationsModal />,
document.getElementById('channel_notifications_modal')
);
React.render(
<ChannelMembersModal />,
document.getElementById('channel_members_modal')
);
React.render(
<ChannelInviteModal />,
document.getElementById('channel_invite_modal')
);
React.render(
<ChannelInfoModal />,
document.getElementById('channel_info_modal')
);
React.render(
<MoreChannelsModal />,
document.getElementById('more_channels_modal')
);
React.render(
<DirectChannelModal />,
document.getElementById('direct_channel_modal')
);
React.render(
<NewChannelModal />,
document.getElementById('new_channel_modal')
);
React.render(
<PostList />,
document.getElementById('post-list')
);
React.render(
<EditPostModal />,
document.getElementById('edit_post_modal')
);
React.render(
<DeletePostModal />,
document.getElementById('delete_post_modal')
);
React.render(
<PostDeletedModal />,
document.getElementById('post_deleted_modal')
);
React.render(
<CreatePost />,
document.getElementById('post-create')
);
React.render(
<SidebarRight />,
document.getElementById('sidebar-right')
);
React.render(
<SidebarRightMenu teamName={team_name} teamType={team_type} />,
document.getElementById('sidebar-menu')
);
React.render(
<MentionList id="post_textbox" />,
document.getElementById('post_mention_tab')
);
React.render(
<MentionList id="reply_textbox" />,
document.getElementById('reply_mention_tab')
);
React.render(
<MentionList id="edit_textbox" />,
document.getElementById('edit_mention_tab')
);
};

13
web/react/pages/find_team.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var FindTeam = require('../components/find_team.jsx');
global.window.setup_find_team_page = function() {
React.render(
<FindTeam />,
document.getElementById('find-team')
);
};

14
web/react/pages/home.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var ChannelStore = require('../stores/channel_store.jsx');
var Constants = require('../utils/constants.jsx');
global.window.setup_home_page = function() {
var last = ChannelStore.getLastVisitedName();
if (last == null || last.length === 0) {
window.location.replace("/channels/" + Constants.DEFAULT_CHANNEL);
} else {
window.location.replace("/channels/" + last);
}
}

11
web/react/pages/login.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Login = require('../components/login.jsx');
global.window.setup_login_page = function() {
React.render(
<Login />,
document.getElementById('login')
);
};

19
web/react/pages/password_reset.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var PasswordReset = require('../components/password_reset.jsx');
global.window.setup_password_reset_page = function(is_reset, team_name, domain, hash, data) {
React.render(
<PasswordReset
isReset={is_reset}
teamName={team_name}
domain={domain}
hash={hash}
data={data}
/>,
document.getElementById('reset')
);
};

11
web/react/pages/signup_team.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SignupTeam =require('../components/signup_team.jsx');
global.window.setup_signup_team_page = function() {
React.render(
<SignupTeam />,
document.getElementById('signup-team')
);
};

11
web/react/pages/signup_team_complete.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SignupTeamComplete =require('../components/signup_team_complete.jsx');
global.window.setup_signup_team_complete_page = function(email, name, data, hash) {
React.render(
<SignupTeamComplete name={name} email={email} hash={hash} data={data} />,
document.getElementById('signup-team-complete')
);
};

11
web/react/pages/signup_user_complete.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SignupUserComplete =require('../components/signup_user_complete.jsx');
global.window.setup_signup_user_complete_page = function(email, domain, name, id, data, hash) {
React.render(
<SignupUserComplete team_id={id} domain={domain} team_name={name} email={email} hash={hash} data={data} />,
document.getElementById('signup-user-complete')
);
};

13
web/react/pages/verify.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var EmailVerify = require('../components/email_verify.jsx');
global.window.setup_verify_page = function(is_verified) {
React.render(
<EmailVerify isVerified={is_verified} />,
document.getElementById('verify')
);
};

255
web/react/stores/channel_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,255 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var MORE_CHANGE_EVENT = 'change';
var EXTRA_INFO_EVENT = 'extra_info';
var ChannelStore = assign({}, EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
emitMoreChange: function() {
this.emit(MORE_CHANGE_EVENT);
},
addMoreChangeListener: function(callback) {
this.on(MORE_CHANGE_EVENT, callback);
},
removeMoreChangeListener: function(callback) {
this.removeListener(MORE_CHANGE_EVENT, callback);
},
emitExtraInfoChange: function() {
this.emit(EXTRA_INFO_EVENT);
},
addExtraInfoChangeListener: function(callback) {
this.on(EXTRA_INFO_EVENT, callback);
},
removeExtraInfoChangeListener: function(callback) {
this.removeListener(EXTRA_INFO_EVENT, callback);
},
get: function(id) {
var current = null;
var c = this._getChannels();
c.some(function(channel) {
if (channel.id == id) {
current = channel;
return true;
}
return false;
});
return current;
},
getMember: function(id) {
var current = null;
return this.getAllMembers()[id];
},
getByName: function(name) {
var current = null;
var c = this._getChannels();
c.some(function(channel) {
if (channel.name == name) {
current = channel;
return true;
}
return false;
});
return current;
},
getAll: function() {
return this._getChannels();
},
getAllMembers: function() {
return this._getChannelMembers();
},
getMoreAll: function() {
return this._getMoreChannels();
},
setCurrentId: function(id) {
if (id == null)
sessionStorage.removeItem("current_channel_id");
else
sessionStorage.setItem("current_channel_id", id);
},
setLastVisitedName: function(name) {
if (name == null)
localStorage.removeItem("last_visited_name");
else
localStorage.setItem("last_visited_name", name);
},
getLastVisitedName: function() {
return localStorage.getItem("last_visited_name");
},
resetCounts: function(id) {
var cm = this._getChannelMembers();
for (var cmid in cm) {
if (cm[cmid].channel_id == id) {
var c = this.get(id);
if (c) {
cm[cmid].msg_count = this.get(id).total_msg_count;
cm[cmid].mention_count = 0;
}
break;
}
}
this._storeChannelMembers(cm);
},
getCurrentId: function() {
return sessionStorage.getItem("current_channel_id");
},
getCurrent: function() {
var currentId = ChannelStore.getCurrentId();
if (currentId != null)
return this.get(currentId);
else
return null;
},
getCurrentMember: function() {
var currentId = ChannelStore.getCurrentId();
if (currentId != null)
return this.getAllMembers()[currentId];
else
return null;
},
setChannelMember: function(member) {
var members = this._getChannelMembers();
members[member.channel_id] = member;
this._storeChannelMembers(members);
this.emitChange();
},
getCurrentExtraInfo: function() {
var currentId = ChannelStore.getCurrentId();
var extra = null;
if (currentId != null)
extra = this._getExtraInfos()[currentId];
if (extra == null)
extra = {members: []};
return extra;
},
getExtraInfo: function(channel_id) {
var extra = null;
if (channel_id != null)
extra = this._getExtraInfos()[channel_id];
if (extra == null)
extra = {members: []};
return extra;
},
_storeChannels: function(channels) {
sessionStorage.setItem("channels", JSON.stringify(channels));
},
_getChannels: function() {
var channels = [];
try {
channels = JSON.parse(sessionStorage.channels);
}
catch (err) {
}
return channels;
},
_storeChannelMembers: function(channelMembers) {
sessionStorage.setItem("channel_members", JSON.stringify(channelMembers));
},
_getChannelMembers: function() {
var members = {};
try {
members = JSON.parse(sessionStorage.channel_members);
}
catch (err) {
}
return members;
},
_storeMoreChannels: function(channels) {
sessionStorage.setItem("more_channels", JSON.stringify(channels));
},
_getMoreChannels: function() {
var channels = [];
try {
channels = JSON.parse(sessionStorage.more_channels);
}
catch (err) {
}
return channels;
},
_storeExtraInfos: function(extraInfos) {
sessionStorage.setItem("extra_infos", JSON.stringify(extraInfos));
},
_getExtraInfos: function() {
var members = {};
try {
members = JSON.parse(sessionStorage.extra_infos);
}
catch (err) {
}
return members;
}
});
ChannelStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.type) {
case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id);
ChannelStore.setLastVisitedName(action.name);
ChannelStore.resetCounts(action.id);
ChannelStore.emitChange();
break;
case ActionTypes.RECIEVED_CHANNELS:
ChannelStore._storeChannels(action.channels);
ChannelStore._storeChannelMembers(action.members);
var currentId = ChannelStore.getCurrentId();
if (currentId) ChannelStore.resetCounts(currentId);
ChannelStore.emitChange();
break;
case ActionTypes.RECIEVED_MORE_CHANNELS:
ChannelStore._storeMoreChannels(action.channels);
ChannelStore.emitMoreChange();
break;
case ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO:
var extra_infos = ChannelStore._getExtraInfos();
extra_infos[action.extra_info.id] = action.extra_info;
ChannelStore._storeExtraInfos(extra_infos);
ChannelStore.emitExtraInfoChange();
break;
default:
}
});
module.exports = ChannelStore;

59
web/react/stores/error_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,59 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var ErrorStore = assign({}, EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
handledError: function() {
sessionStorage.removeItem("last_error");
},
getLastError: function() {
var error = null;
try {
error = JSON.parse(sessionStorage.last_error);
}
catch (err) {
}
return error;
},
_storeLastError: function(error) {
sessionStorage.setItem("last_error", JSON.stringify(error));
},
});
ErrorStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.type) {
case ActionTypes.RECIEVED_ERROR:
ErrorStore._storeLastError(action.err);
ErrorStore.emitChange();
break;
default:
}
});
module.exports = ErrorStore;

224
web/react/stores/post_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,224 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var SEARCH_CHANGE_EVENT = 'search_change';
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
var ADD_MENTION_EVENT = 'add_mention';
var PostStore = assign({}, EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
emitSearchChange: function() {
this.emit(SEARCH_CHANGE_EVENT);
},
addSearchChangeListener: function(callback) {
this.on(SEARCH_CHANGE_EVENT, callback);
},
removeSearchChangeListener: function(callback) {
this.removeListener(SEARCH_CHANGE_EVENT, callback);
},
emitSearchTermChange: function(doSearch, isMentionSearch) {
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
},
addSearchTermChangeListener: function(callback) {
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
},
removeSearchTermChangeListener: function(callback) {
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
},
emitSelectedPostChange: function(from_search) {
this.emit(SELECTED_POST_CHANGE_EVENT, from_search);
},
addSelectedPostChangeListener: function(callback) {
this.on(SELECTED_POST_CHANGE_EVENT, callback);
},
removeSelectedPostChangeListener: function(callback) {
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
},
emitMentionDataChange: function(id, mentionText, excludeList) {
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText, excludeList);
},
addMentionDataChangeListener: function(callback) {
this.on(MENTION_DATA_CHANGE_EVENT, callback);
},
removeMentionDataChangeListener: function(callback) {
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
},
emitAddMention: function(id, username) {
this.emit(ADD_MENTION_EVENT, id, username);
},
addAddMentionListener: function(callback) {
this.on(ADD_MENTION_EVENT, callback);
},
removeAddMentionListener: function(callback) {
this.removeListener(ADD_MENTION_EVENT, callback);
},
getCurrentPosts: function() {
var currentId = ChannelStore.getCurrentId();
if (currentId != null)
return this.getPosts(currentId);
else
return null;
},
storePosts: function(channelId, posts) {
this._storePosts(channelId, posts);
this.emitChange();
},
_storePosts: function(channelId, posts) {
sessionStorage.setItem("posts_" + channelId, JSON.stringify(posts));
},
getPosts: function(channelId) {
var posts = null;
try {
posts = JSON.parse(sessionStorage.getItem("posts_" + channelId));
}
catch (err) {
}
return posts;
},
storeSearchResults: function(results, is_mention_search) {
sessionStorage.setItem("search_results", JSON.stringify(results));
is_mention_search = is_mention_search ? true : false; // force to bool
sessionStorage.setItem("is_mention_search", JSON.stringify(is_mention_search));
},
getSearchResults: function() {
var results = null;
try {
results = JSON.parse(sessionStorage.getItem("search_results"));
}
catch (err) {
}
return results;
},
getIsMentionSearch: function() {
var result = false;
try {
result = JSON.parse(sessionStorage.getItem("is_mention_search"));
}
catch (err) {
}
return result;
},
storeSelectedPost: function(post_list) {
sessionStorage.setItem("select_post", JSON.stringify(post_list));
},
getSelectedPost: function() {
var post_list = null;
try {
post_list = JSON.parse(sessionStorage.getItem("select_post"));
}
catch (err) {
}
return post_list;
},
storeSearchTerm: function(term) {
sessionStorage.setItem("search_term", term);
},
getSearchTerm: function() {
return sessionStorage.getItem("search_term");
},
storeCurrentDraft: function(draft) {
var channel_id = ChannelStore.getCurrentId();
var user_id = UserStore.getCurrentId();
localStorage.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft));
},
getCurrentDraft: function() {
var channel_id = ChannelStore.getCurrentId();
var user_id = UserStore.getCurrentId();
return JSON.parse(localStorage.getItem("draft_" + channel_id + "_" + user_id));
},
storeDraft: function(channel_id, user_id, draft) {
localStorage.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft));
},
getDraft: function(channel_id, user_id) {
return JSON.parse(localStorage.getItem("draft_" + channel_id + "_" + user_id));
},
clearDraftUploads: function() {
for (key in localStorage) {
if (key.substring(0,6) === "draft_") {
var d = JSON.parse(localStorage.getItem(key));
if (d) {
d['uploadsInProgress'] = 0;
localStorage.setItem(key, JSON.stringify(d));
}
}
}
}
});
PostStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.type) {
case ActionTypes.RECIEVED_POSTS:
PostStore._storePosts(action.id, action.post_list);
PostStore.emitChange();
break;
case ActionTypes.RECIEVED_SEARCH:
PostStore.storeSearchResults(action.results, action.is_mention_search);
PostStore.emitSearchChange();
break;
case ActionTypes.RECIEVED_SEARCH_TERM:
PostStore.storeSearchTerm(action.term);
PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
break;
case ActionTypes.RECIEVED_POST_SELECTED:
PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search);
break;
case ActionTypes.RECIEVED_MENTION_DATA:
PostStore.emitMentionDataChange(action.id, action.mention_text, action.exclude_list);
break;
case ActionTypes.RECIEVED_ADD_MENTION:
PostStore.emitAddMention(action.id, action.username);
break;
default:
}
});
module.exports = PostStore;

86
web/react/stores/socket_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,86 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var UserStore = require('./user_store.jsx')
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var client = require('../utils/client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var conn;
var SocketStore = assign({}, EventEmitter.prototype, {
initialize: function(self) {
if (!UserStore.getCurrentId()) return;
if (!self) self = this;
self.setMaxListeners(0);
if (window["WebSocket"] && !conn) {
var protocol = window.location.protocol == "https:" ? "wss://" : "ws://";
var port = window.location.protocol == "https:" ? ":8443" : "";
var conn_url = protocol + location.host + port + "/api/v1/websocket";
console.log("connecting to " + conn_url);
conn = new WebSocket(conn_url);
conn.onclose = function(evt) {
console.log("websocket closed");
console.log(evt);
conn = null;
setTimeout(function(){self.initialize(self)}, 3000);
};
conn.onerror = function(evt) {
console.log("websocket error");
console.log(evt);
};
conn.onmessage = function(evt) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_MSG,
msg: JSON.parse(evt.data)
});
};
}
},
emitChange: function(msg) {
this.emit(CHANGE_EVENT, msg);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
sendMessage: function (msg) {
if (conn && conn.readyState === WebSocket.OPEN) {
conn.send(JSON.stringify(msg));
} else if (!conn || conn.readyState === WebSocket.Closed) {
conn = null;
this.initialize();
}
}
});
SocketStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.type) {
case ActionTypes.RECIEVED_MSG:
SocketStore.emitChange(action.msg);
break;
default:
}
});
SocketStore.initialize();
module.exports = SocketStore;

328
web/react/stores/user_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,328 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var client = require('../utils/client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var CHANGE_EVENT_SESSIONS = 'change_sessions';
var CHANGE_EVENT_AUDITS = 'change_audits';
var CHANGE_EVENT_TEAMS = 'change_teams';
var CHANGE_EVENT_STATUSES = 'change_statuses';
var UserStore = assign({}, EventEmitter.prototype, {
emitChange: function(userId) {
this.emit(CHANGE_EVENT, userId);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
emitSessionsChange: function() {
this.emit(CHANGE_EVENT_SESSIONS);
},
addSessionsChangeListener: function(callback) {
this.on(CHANGE_EVENT_SESSIONS, callback);
},
removeSessionsChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT_SESSIONS, callback);
},
emitAuditsChange: function() {
this.emit(CHANGE_EVENT_AUDITS);
},
addAuditsChangeListener: function(callback) {
this.on(CHANGE_EVENT_AUDITS, callback);
},
removeAuditsChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT_AUDITS, callback);
},
emitTeamsChange: function() {
this.emit(CHANGE_EVENT_TEAMS);
},
addTeamsChangeListener: function(callback) {
this.on(CHANGE_EVENT_TEAMS, callback);
},
removeTeamsChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT_TEAMS, callback);
},
emitStatusesChange: function() {
this.emit(CHANGE_EVENT_STATUSES);
},
addStatusesChangeListener: function(callback) {
this.on(CHANGE_EVENT_STATUSES, callback);
},
removeStatusesChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT_STATUSES, callback);
},
setCurrentId: function(id) {
if (id == null)
sessionStorage.removeItem("current_user_id");
else
sessionStorage.setItem("current_user_id", id);
},
getCurrentId: function(skipFetch) {
var current_id = sessionStorage.current_user_id;
// this is a speical case to force fetch the
// current user if it's missing
// it's synchronous to block rendering
if (current_id == null && !skipFetch) {
var me = client.getMeSynchronous();
if (me != null) {
this.setCurrentUser(me);
current_id = me.id;
}
}
return current_id;
},
getCurrentUser: function(skipFetch) {
if (this.getCurrentId(skipFetch) == null) {
return null;
}
return this._getProfiles()[this.getCurrentId()];
},
setCurrentUser: function(user) {
this.saveProfile(user);
this.setCurrentId(user.id);
},
getLastDomain: function() {
return localStorage.last_domain;
},
setLastDomain: function(domain) {
localStorage.setItem("last_domain", domain);
},
getLastEmail: function() {
return localStorage.last_email;
},
setLastEmail: function(email) {
localStorage.setItem("last_email", email);
},
removeCurrentUser: function() {
this.setCurrentId(null);
},
hasProfile: function(userId) {
return this._getProfiles()[userId] != null;
},
getProfile: function(userId) {
return this._getProfiles()[userId];
},
getProfileByUsername: function(username) {
return this._getProfilesUsernameMap()[username];
},
getProfilesUsernameMap: function() {
return this._getProfilesUsernameMap();
},
getProfiles: function() {
return this._getProfiles();
},
getActiveOnlyProfiles: function() {
active = {};
current = this._getProfiles();
for (var key in current) {
if (current[key].delete_at == 0) {
active[key] = current[key];
}
}
return active;
},
saveProfile: function(profile) {
var ps = this._getProfiles();
ps[profile.id] = profile;
this._storeProfiles(ps);
},
_storeProfiles: function(profiles) {
sessionStorage.setItem("profiles", JSON.stringify(profiles));
var profileUsernameMap = {};
for (var id in profiles) {
profileUsernameMap[profiles[id].username] = profiles[id];
}
sessionStorage.setItem("profileUsernameMap", JSON.stringify(profileUsernameMap));
},
_getProfiles: function() {
var profiles = {};
try {
profiles = JSON.parse(sessionStorage.getItem("profiles"));
if (profiles == null) {
profiles = {};
}
}
catch (err) {
}
return profiles;
},
_getProfilesUsernameMap: function() {
var profileUsernameMap = {};
try {
profileUsernameMap = JSON.parse(sessionStorage.getItem("profileUsernameMap"));
if (profileUsernameMap == null) {
profileUsernameMap = {};
}
}
catch (err) {
}
return profileUsernameMap;
},
setSessions: function(sessions) {
sessionStorage.setItem("sessions", JSON.stringify(sessions));
},
getSessions: function() {
var sessions = [];
try {
sessions = JSON.parse(sessionStorage.getItem("sessions"));
if (sessions == null) {
sessions = [];
}
}
catch (err) {
}
return sessions;
},
setAudits: function(audits) {
sessionStorage.setItem("audits", JSON.stringify(audits));
},
getAudits: function() {
var audits = [];
try {
audits = JSON.parse(sessionStorage.getItem("audits"));
if (audits == null) {
audits = [];
}
}
catch (err) {
}
return audits;
},
setTeams: function(teams) {
sessionStorage.setItem("teams", JSON.stringify(teams));
},
getTeams: function() {
var teams = [];
try {
teams = JSON.parse(sessionStorage.getItem("teams"));
if (teams == null) {
teams = [];
}
}
catch (err) {
}
return teams;
},
getCurrentMentionKeys: function() {
var user = this.getCurrentUser();
if (user.notify_props && user.notify_props.mention_keys) {
var keys = user.notify_props.mention_keys.split(',');
if (user.full_name.length > 0 && user.notify_props.first_name === "true") {
var first = user.full_name.split(' ')[0];
if (first.length > 0) keys.push(first);
}
return keys;
} else {
return [];
}
},
getLastVersion: function() {
return sessionStorage.last_version;
},
setLastVersion: function(version) {
sessionStorage.setItem("last_version", version);
},
setStatuses: function(statuses) {
this._setStatuses(statuses);
this.emitStatusesChange();
},
_setStatuses: function(statuses) {
sessionStorage.setItem("statuses", JSON.stringify(statuses));
},
setStatus: function(user_id, status) {
var statuses = this.getStatuses();
statuses[user_id] = status;
this._setStatuses(statuses);
this.emitStatusesChange();
},
getStatuses: function() {
var statuses = {};
try {
statuses = JSON.parse(sessionStorage.getItem("statuses"));
if (statuses == null) {
statuses = {};
}
}
catch (err) {
}
return statuses;
},
getStatus: function(id) {
return this.getStatuses()[id];
}
});
UserStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.type) {
case ActionTypes.RECIEVED_PROFILES:
for(var id in action.profiles) {
// profiles can have incomplete data, so don't overwrite current user
if (id === UserStore.getCurrentId()) continue;
var profile = action.profiles[id];
UserStore.saveProfile(profile);
UserStore.emitChange(profile.id);
}
break;
case ActionTypes.RECIEVED_ME:
UserStore.setCurrentUser(action.me);
UserStore.emitChange(action.me.id);
break;
case ActionTypes.RECIEVED_SESSIONS:
UserStore.setSessions(action.sessions);
UserStore.emitSessionsChange();
break;
case ActionTypes.RECIEVED_AUDITS:
UserStore.setAudits(action.audits);
UserStore.emitAuditsChange();
break;
case ActionTypes.RECIEVED_TEAMS:
UserStore.setTeams(action.teams);
UserStore.emitTeamsChange();
break;
case ActionTypes.RECIEVED_STATUSES:
UserStore._setStatuses(action.statuses);
UserStore.emitStatusesChange();
break;
default:
}
});
UserStore.setMaxListeners(0);
global.window.UserStore = UserStore;
module.exports = UserStore;

357
web/react/utils/async_client.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,357 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var client = require('./client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var utils = require('./utils.jsx');
var Constants = require('./constants.jsx');
var ActionTypes = Constants.ActionTypes;
// Used to track in progress async calls
var callTracker = {};
var dispatchError = function(err, method) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ERROR,
err: err,
method: method
});
};
var isCallInProgress = function(callName) {
if (!(callName in callTracker)) return false;
if (callTracker[callName] === 0) return false;
if (utils.getTimestamp() - callTracker[callName] > 5000) {
console.log("AsyncClient call " + callName + " expired after more than 5 seconds");
return false;
}
return true;
};
module.exports.dispatchError = dispatchError;
module.exports.getChannels = function(force, updateLastViewed, checkVersion) {
if (isCallInProgress("getChannels")) return;
if (ChannelStore.getAll().length == 0 || force) {
callTracker["getChannels"] = utils.getTimestamp();
client.getChannels(
function(data, textStatus, xhr) {
callTracker["getChannels"] = 0;
if (updateLastViewed && ChannelStore.getCurrentId() != null) {
module.exports.updateLastViewedAt();
}
if (checkVersion) {
var serverVersion = xhr.getResponseHeader("X-Version-ID");
if (UserStore.getLastVersion() == undefined) {
UserStore.setLastVersion(serverVersion);
}
if (serverVersion != UserStore.getLastVersion()) {
UserStore.setLastVersion(serverVersion);
window.location.href = window.location.href;
console.log("Detected version update refreshing the page");
}
}
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CHANNELS,
channels: data.channels,
members: data.members
});
},
function(err) {
callTracker["getChannels"] = 0;
dispatchError(err, "getChannels");
}
);
}
}
module.exports.updateLastViewedAt = function() {
if (isCallInProgress("updateLastViewed")) return;
if (ChannelStore.getCurrentId() == null) return;
callTracker["updateLastViewed"] = utils.getTimestamp();
client.updateLastViewedAt(
ChannelStore.getCurrentId(),
function(data) {
callTracker["updateLastViewed"] = 0;
},
function(err) {
callTracker["updateLastViewed"] = 0;
dispatchError(err, "updateLastViewedAt");
}
);
}
module.exports.getMoreChannels = function(force) {
if (isCallInProgress("getMoreChannels")) return;
if (ChannelStore.getMoreAll().length == 0 || force) {
callTracker["getMoreChannels"] = utils.getTimestamp();
client.getMoreChannels(
function(data, textStatus, xhr) {
callTracker["getMoreChannels"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_MORE_CHANNELS,
channels: data.channels,
members: data.members
});
},
function(err) {
callTracker["getMoreChannels"] = 0;
dispatchError(err, "getMoreChannels");
}
);
}
}
module.exports.getChannelExtraInfo = function(force) {
var channelId = ChannelStore.getCurrentId();
if (channelId != null) {
if (isCallInProgress("getChannelExtraInfo_"+channelId)) return;
var minMembers = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D' ? 1 : 0;
if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) {
callTracker["getChannelExtraInfo_"+channelId] = utils.getTimestamp();
client.getChannelExtraInfo(
channelId,
function(data, textStatus, xhr) {
callTracker["getChannelExtraInfo_"+channelId] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
extra_info: data
});
},
function(err) {
callTracker["getChannelExtraInfo_"+channelId] = 0;
dispatchError(err, "getChannelExtraInfo");
}
);
}
}
}
module.exports.getProfiles = function() {
if (isCallInProgress("getProfiles")) return;
callTracker["getProfiles"] = utils.getTimestamp();
client.getProfiles(
function(data, textStatus, xhr) {
callTracker["getProfiles"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_PROFILES,
profiles: data
});
},
function(err) {
callTracker["getProfiles"] = 0;
dispatchError(err, "getProfiles");
}
);
}
module.exports.getSessions = function() {
if (isCallInProgress("getSessions")) return;
callTracker["getSessions"] = utils.getTimestamp();
client.getSessions(
UserStore.getCurrentId(),
function(data, textStatus, xhr) {
callTracker["getSessions"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SESSIONS,
sessions: data
});
},
function(err) {
callTracker["getSessions"] = 0;
dispatchError(err, "getSessions");
}
);
}
module.exports.getAudits = function() {
if (isCallInProgress("getAudits")) return;
callTracker["getAudits"] = utils.getTimestamp();
client.getAudits(
UserStore.getCurrentId(),
function(data, textStatus, xhr) {
callTracker["getAudits"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_AUDITS,
audits: data
});
},
function(err) {
callTracker["getAudits"] = 0;
dispatchError(err, "getAudits");
}
);
}
module.exports.findTeams = function(email) {
if (isCallInProgress("findTeams_"+email)) return;
var user = UserStore.getCurrentUser();
if (user) {
callTracker["findTeams_"+email] = utils.getTimestamp();
client.findTeams(
user.email,
function(data, textStatus, xhr) {
callTracker["findTeams_"+email] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_TEAMS,
teams: data
});
},
function(err) {
callTracker["findTeams_"+email] = 0;
dispatchError(err, "findTeams");
}
);
}
}
module.exports.search = function(terms) {
if (isCallInProgress("search_"+String(terms))) return;
callTracker["search_"+String(terms)] = utils.getTimestamp();
client.search(
terms,
function(data, textStatus, xhr) {
callTracker["search_"+String(terms)] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH,
results: data
});
},
function(err) {
callTracker["search_"+String(terms)] = 0;
dispatchError(err, "search");
}
);
}
module.exports.getPosts = function(force, id) {
if (PostStore.getCurrentPosts() == null || force) {
var channelId = id ? id : ChannelStore.getCurrentId();
if (isCallInProgress("getPosts_"+channelId)) return;
var post_list = PostStore.getCurrentPosts();
// if we already have more than POST_CHUNK_SIZE posts,
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
// with a max at 180
var numPosts = post_list && post_list.order.length > 0 ? Math.min(180, Constants.POST_CHUNK_SIZE * Math.ceil(post_list.order.length / Constants.POST_CHUNK_SIZE)) : Constants.POST_CHUNK_SIZE;
if (channelId != null) {
callTracker["getPosts_"+channelId] = utils.getTimestamp();
client.getPosts(
channelId,
0,
numPosts,
function(data, textStatus, xhr) {
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
post_list: data
});
module.exports.getProfiles();
},
function(err) {
dispatchError(err, "getPosts");
},
function() {
callTracker["getPosts_"+channelId] = 0;
}
);
}
}
}
module.exports.getMe = function() {
if (isCallInProgress("getMe")) return;
callTracker["getMe"] = utils.getTimestamp();
client.getMeSynchronous(
function(data, textStatus, xhr) {
callTracker["getMe"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ME,
me: data
});
},
function(err) {
callTracker["getMe"] = 0;
dispatchError(err, "getMe");
}
);
}
module.exports.getStatuses = function() {
if (isCallInProgress("getStatuses")) return;
callTracker["getStatuses"] = utils.getTimestamp();
client.getStatuses(
function(data, textStatus, xhr) {
callTracker["getStatuses"] = 0;
if (xhr.status === 304 || !data) return;
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_STATUSES,
statuses: data
});
},
function(err) {
callTracker["getStatuses"] = 0;
dispatchError(err, "getStatuses");
}
);
}

813
web/react/utils/client.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,813 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
module.exports.track = function(category, action, label, prop, val) {
global.window.snowplow('trackStructEvent', category, action, label, prop, val);
if (global.window.analytics != null) global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
};
module.exports.trackPage = function() {
global.window.snowplow('trackPageView');
if (global.window.analytics != null) global.window.analytics.page();
};
function handleError(method_name, xhr, status, err) {
var _LTracker = global.window._LTracker || [];
var e = null;
try {
e = JSON.parse(xhr.responseText);
}
catch(parse_error) {
}
var msg = "";
if (e) {
msg = "error in " + method_name + " msg=" + e.message + " detail=" + e.detailed_error + " rid=" + e.request_id;
}
else {
msg = "error in " + method_name + " status=" + status + " statusCode=" + xhr.status + " err=" + err;
if (xhr.status === 0)
e = { message: "There appears to be a problem with your internet connection" };
else
e = { message: "We received an unexpected status code from the server (" + xhr.status + ")"};
}
console.error(msg)
console.error(e);
_LTracker.push(msg);
module.exports.track('api', 'api_weberror', method_name, 'message', msg);
if (xhr.status == 401) {
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname+window.location.search);
}
return e;
}
module.exports.createTeamFromSignup = function(team_signup, success, error) {
$.ajax({
url: "/api/v1/teams/create_from_signup",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(team_signup),
success: success,
error: function(xhr, status, err) {
e = handleError("createTeamFromSignup", xhr, status, err);
error(e);
}
});
};
module.exports.createUser = function(user, data, email_hash, success, error) {
$.ajax({
url: "/api/v1/users/create?d=" + encodeURIComponent(data) + "&h=" + encodeURIComponent(email_hash),
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(user),
success: success,
error: function(xhr, status, err) {
e = handleError("createUser", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_create', user.team_id, 'email', user.email);
};
module.exports.updateUser = function(user, success, error) {
$.ajax({
url: "/api/v1/users/update",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(user),
success: success,
error: function(xhr, status, err) {
e = handleError("updateUser", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_update');
};
module.exports.updatePassword = function(data, success, error) {
$.ajax({
url: "/api/v1/users/newpassword",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("newPassword", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_newpassword');
};
module.exports.updateUserNotifyProps = function(data, success, error) {
$.ajax({
url: "/api/v1/users/update_notify",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateUserNotifyProps", xhr, status, err);
error(e);
}
});
};
module.exports.updateRoles = function(data, success, error) {
$.ajax({
url: "/api/v1/users/update_roles",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateRoles", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_update_roles');
};
module.exports.updateActive = function(userId, active, success, error) {
var data = {};
data["user_id"] = userId;
data["active"] = "" + active;
$.ajax({
url: "/api/v1/users/update_active",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateActive", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_update_roles');
};
module.exports.sendPasswordReset = function(data, success, error) {
$.ajax({
url: "/api/v1/users/send_password_reset",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("sendPasswordReset", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_send_password_reset');
};
module.exports.resetPassword = function(data, success, error) {
$.ajax({
url: "/api/v1/users/reset_password",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("resetPassword", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_reset_password');
};
module.exports.logout = function() {
module.exports.track('api', 'api_users_logout');
sessionStorage.clear();
window.location.href = "/logout";
};
module.exports.loginByEmail = function(domain, email, password, success, error) {
$.ajax({
url: "/api/v1/users/login",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({domain: domain, email: email, password: password}),
success: function(data, textStatus, xhr) {
module.exports.track('api', 'api_users_login_success', data.team_id, 'email', data.email);
success(data, textStatus, xhr);
},
error: function(xhr, status, err) {
module.exports.track('api', 'api_users_login_fail', window.getSubDomain(), 'email', email);
e = handleError("loginByEmail", xhr, status, err);
error(e);
}
});
};
module.exports.revokeSession = function(altId, success, error) {
$.ajax({
url: "/api/v1/users/revoke_session",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({id: altId}),
success: success,
error: function(xhr, status, err) {
e = handleError("revokeSession", xhr, status, err);
error(e);
}
});
};
module.exports.getSessions = function(userId, success, error) {
$.ajax({
url: "/api/v1/users/"+userId+"/sessions",
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: function(xhr, status, err) {
e = handleError("getSessions", xhr, status, err);
error(e);
}
});
};
module.exports.getAudits = function(userId, success, error) {
$.ajax({
url: "/api/v1/users/"+userId+"/audits",
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: function(xhr, status, err) {
e = handleError("getAudits", xhr, status, err);
error(e);
}
});
};
module.exports.getMeSynchronous = function(success, error) {
var current_user = null;
$.ajax({
async: false,
url: "/api/v1/users/me",
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: function(data, textStatus, xhr) {
current_user = data;
if (success) success(data, textStatus, xhr);
},
error: function(xhr, status, err) {
if (error) {
e = handleError("getMeSynchronous", xhr, status, err);
error(e);
};
}
});
return current_user;
};
module.exports.inviteMembers = function(data, success, error) {
$.ajax({
url: "/api/v1/teams/invite_members",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("inviteMembers", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_teams_invite_members');
};
module.exports.updateTeamName = function(data, success, error) {
$.ajax({
url: "/api/v1/teams/update_name",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateTeamName", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_teams_update_name');
};
module.exports.signupTeam = function(email, name, success, error) {
$.ajax({
url: "/api/v1/teams/signup",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email, name: name}),
success: success,
error: function(xhr, status, err) {
e = handleError("singupTeam", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_teams_signup');
};
module.exports.createTeam = function(team, success, error) {
$.ajax({
url: "/api/v1/teams/create",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(team),
success: success,
error: function(xhr, status, err) {
e = handleError("createTeam", xhr, status, err);
error(e);
}
});
};
module.exports.findTeamByDomain = function(domain, success, error) {
$.ajax({
url: "/api/v1/teams/find_team_by_domain",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({domain: domain}),
success: success,
error: function(xhr, status, err) {
e = handleError("findTeamByDomain", xhr, status, err);
error(e);
}
});
};
module.exports.findTeamsSendEmail = function(email, success, error) {
$.ajax({
url: "/api/v1/teams/email_teams",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email}),
success: success,
error: function(xhr, status, err) {
e = handleError("findTeamsSendEmail", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_teams_email_teams');
};
module.exports.findTeams = function(email, success, error) {
$.ajax({
url: "/api/v1/teams/find_teams",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email}),
success: success,
error: function(xhr, status, err) {
e = handleError("findTeams", xhr, status, err);
error(e);
}
});
};
module.exports.createChannel = function(channel, success, error) {
$.ajax({
url: "/api/v1/channels/create",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(channel),
success: success,
error: function(xhr, status, err) {
e = handleError("createChannel", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_create', channel.type, 'name', channel.name);
};
module.exports.updateChannel = function(channel, success, error) {
$.ajax({
url: "/api/v1/channels/update",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(channel),
success: success,
error: function(xhr, status, err) {
e = handleError("updateChannel", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_update');
};
module.exports.updateChannelDesc = function(data, success, error) {
$.ajax({
url: "/api/v1/channels/update_desc",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateChannelDesc", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_desc');
};
module.exports.updateNotifyLevel = function(data, success, error) {
$.ajax({
url: "/api/v1/channels/update_notify_level",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("updateNotifyLevel", xhr, status, err);
error(e);
}
});
};
module.exports.joinChannel = function(id, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/join",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
success: success,
error: function(xhr, status, err) {
e = handleError("joinChannel", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_join');
};
module.exports.leaveChannel = function(id, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/leave",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
success: success,
error: function(xhr, status, err) {
e = handleError("leaveChannel", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_leave');
};
module.exports.deleteChannel = function(id, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/delete",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
success: success,
error: function(xhr, status, err) {
e = handleError("deleteChannel", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_delete');
};
module.exports.updateLastViewedAt = function(channelId, success, error) {
$.ajax({
url: "/api/v1/channels/" + channelId + "/update_last_viewed_at",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
success: success,
error: function(xhr, status, err) {
e = handleError("updateLastViewedAt", xhr, status, err);
error(e);
}
});
};
module.exports.getChannels = function(success, error) {
$.ajax({
url: "/api/v1/channels/",
dataType: 'json',
type: 'GET',
success: success,
ifModified: true,
error: function(xhr, status, err) {
e = handleError("getChannels", xhr, status, err);
error(e);
}
});
};
module.exports.getMoreChannels = function(success, error) {
$.ajax({
url: "/api/v1/channels/more",
dataType: 'json',
type: 'GET',
success: success,
ifModified: true,
error: function(xhr, status, err) {
e = handleError("getMoreChannels", xhr, status, err);
error(e);
}
});
};
module.exports.getChannelExtraInfo = function(id, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/extra_info",
dataType: 'json',
type: 'GET',
success: success,
error: function(xhr, status, err) {
e = handleError("getChannelExtraInfo", xhr, status, err);
error(e);
}
});
};
module.exports.executeCommand = function(channelId, command, suggest, success, error) {
$.ajax({
url: "/api/v1/command",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({channelId: channelId, command: command, suggest: "" + suggest}),
success: success,
error: function(xhr, status, err) {
e = handleError("executeCommand", xhr, status, err);
error(e);
}
});
};
module.exports.getPosts = function(channelId, offset, limit, success, error, complete) {
$.ajax({
url: "/api/v1/channels/" + channelId + "/posts/" + offset + "/" + limit,
dataType: 'json',
type: 'GET',
ifModified: true,
success: success,
error: function(xhr, status, err) {
try {
e = handleError("getPosts", xhr, status, err);
error(e);
} catch(er) {
console.error(er);
}
},
complete: complete
});
};
module.exports.getPost = function(channelId, postId, success, error) {
$.ajax({
url: "/api/v1/channels/" + channelId + "/post/" + postId,
dataType: 'json',
type: 'GET',
ifModified: false,
success: success,
error: function(xhr, status, err) {
e = handleError("getPost", xhr, status, err);
error(e);
}
});
};
module.exports.search = function(terms, success, error) {
$.ajax({
url: "/api/v1/posts/search",
dataType: 'json',
type: 'GET',
data: {"terms": terms},
success: success,
error: function(xhr, status, err) {
e = handleError("search", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_posts_search');
};
module.exports.deletePost = function(channelId, id, success, error) {
$.ajax({
url: "/api/v1/channels/" + channelId + "/post/" + id + "/delete",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
success: success,
error: function(xhr, status, err) {
e = handleError("deletePost", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_posts_delete');
};
module.exports.createPost = function(post, channel, success, error) {
$.ajax({
url: "/api/v1/channels/"+ post.channel_id + "/create",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(post),
success: success,
error: function(xhr, status, err) {
e = handleError("createPost", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_posts_create', channel.name, 'length', post.message.length);
// global.window.analytics.track('api_posts_create', {
// category: 'api',
// channel_name: channel.name,
// channel_type: channel.type,
// length: post.message.length,
// files: (post.filenames || []).length,
// mentions: (post.message.match("/<mention>/g") || []).length
// });
};
module.exports.updatePost = function(post, success, error) {
$.ajax({
url: "/api/v1/channels/"+ post.channel_id + "/update",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(post),
success: success,
error: function(xhr, status, err) {
e = handleError("updatePost", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_posts_update');
};
module.exports.addChannelMember = function(id, data, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/add",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("addChannelMember", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_add_member');
};
module.exports.removeChannelMember = function(id, data, success, error) {
$.ajax({
url: "/api/v1/channels/" + id + "/remove",
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("removeChannelMember", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_channels_remove_member');
};
module.exports.getProfiles = function(success, error) {
$.ajax({
url: "/api/v1/users/profiles",
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
ifModified: true,
error: function(xhr, status, err) {
e = handleError("getProfiles", xhr, status, err);
error(e);
}
});
};
module.exports.uploadFile = function(formData, success, error) {
$.ajax({
url: "/api/v1/files/upload",
type: 'POST',
data: formData,
cache: false,
contentType: false,
processData: false,
success: success,
error: function(xhr, status, err) {
e = handleError("uploadFile", xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_files_upload');
};
module.exports.getPublicLink = function(data, success, error) {
$.ajax({
url: "/api/v1/files/get_public_link",
dataType: 'json',
type: 'POST',
data: JSON.stringify(data),
success: success,
error: function(xhr, status, err) {
e = handleError("getPublicLink", xhr, status, err);
error(e);
}
});
};
module.exports.uploadProfileImage = function(imageData, success, error) {
$.ajax({
url: "/api/v1/users/newimage",
type: 'POST',
data: imageData,
cache: false,
contentType: false,
processData: false,
success: success,
error: function(xhr, status, err) {
e = handleError("uploadProfileImage", xhr, status, err);
error(e);
}
});
};
module.exports.getStatuses = function(success, error) {
$.ajax({
url: "/api/v1/users/status",
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: function(xhr, status, err) {
e = handleError("getStatuses", xhr, status, err);
error(e);
}
});
};

78
web/react/utils/constants.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,78 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var keyMirror = require('keymirror');
module.exports = {
ActionTypes: keyMirror({
RECIEVED_ERROR: null,
CLICK_CHANNEL: null,
CREATE_CHANNEL: null,
RECIEVED_CHANNELS: null,
RECIEVED_MORE_CHANNELS: null,
RECIEVED_CHANNEL_EXTRA_INFO: null,
RECIEVED_POSTS: null,
RECIEVED_SEARCH: null,
RECIEVED_POST_SELECTED: null,
RECIEVED_MENTION_DATA: null,
RECIEVED_ADD_MENTION: null,
RECIEVED_PROFILES: null,
RECIEVED_ME: null,
RECIEVED_SESSIONS: null,
RECIEVED_AUDITS: null,
RECIEVED_TEAMS: null,
RECIEVED_STATUSES: null,
RECIEVED_MSG: null,
}),
PayloadSources: keyMirror({
SERVER_ACTION: null,
VIEW_ACTION: null
}),
CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'],
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
SPREADSHEET_TYPES: ['ppt', 'pptx', 'csv'],
EXCEL_TYPES: ['xlsx'],
WORD_TYPES: ['doc', 'docx'],
CODE_TYPES: ['css', 'html', 'js', 'php', 'rb'],
PDF_TYPES: ['pdf'],
PATCH_TYPES: ['patch'],
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
MAX_DISPLAY_FILES: 5,
MAX_FILE_SIZE: 50000000, // 50 MB
DEFAULT_CHANNEL: 'town-square',
POST_CHUNK_SIZE: 60,
RESERVED_DOMAINS: [
"www",
"web",
"admin",
"support",
"notify",
"test",
"demo",
"mail",
"team",
"channel",
"internal",
"localhost",
"stag",
"post",
"cluster",
"api",
],
RESERVED_USERNAMES: [
"valet",
"all",
"channel",
],
MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
MAX_DMS: 10,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>"
};

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

@@ -0,0 +1,732 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var AsyncClient = require('./async_client.jsx');
var client = require('./client.jsx');
var LinkifyIt = require('linkify-it');
module.exports.isEmail = function(email) {
var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
return regex.test(email);
};
module.exports.cleanUpUrlable = function(input) {
var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
cleaned = cleaned.replace(/^\-+/, '');
cleaned = cleaned.replace(/\-+$/, '');
return cleaned;
};
module.exports.isTestDomain = function() {
if ((/^localhost/).test(window.location.hostname))
return true;
if ((/^test/).test(window.location.hostname))
return true;
if ((/^127.0./).test(window.location.hostname))
return true;
if ((/^192.168./).test(window.location.hostname))
return true;
if ((/^10./).test(window.location.hostname))
return true;
if ((/^176./).test(window.location.hostname))
return true;
return false;
};
var getSubDomain = function() {
if (module.exports.isTestDomain())
return "";
if ((/^www/).test(window.location.hostname))
return "";
if ((/^beta/).test(window.location.hostname))
return "";
if ((/^ci/).test(window.location.hostname))
return "";
var parts = window.location.hostname.split(".");
if (parts.length != 3)
return "";
return parts[0];
}
global.window.getSubDomain = getSubDomain;
module.exports.getSubDomain = getSubDomain;
module.exports.getDomainWithOutSub = function() {
var parts = window.location.host.split(".");
if (parts.length == 1)
return "localhost:8065";
return parts[1] + "." + parts[2];
}
module.exports.getCookie = function(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
module.exports.notifyMe = function(title, body, channel) {
if ("Notification" in window && Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
if (Notification.permission !== permission) {
Notification.permission = permission;
}
if (permission === "granted") {
var notification = new Notification(title,
{ body: body, tag: body, icon: '/static/images/icon50x50.gif' }
);
notification.onclick = function() {
window.focus();
if (channel) {
module.exports.switchChannel(channel);
} else {
window.location.href = "/channels/town-square";
}
};
setTimeout(function(){
notification.close();
}, 5000);
}
});
}
}
module.exports.ding = function() {
var audio = new Audio('/static/images/ding.mp3');
audio.play();
}
module.exports.getUrlParameter = function(sParam) {
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++)
{
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == sParam)
{
return sParameterName[1];
}
}
return null;
}
module.exports.getDateForUnixTicks = function(ticks) {
return new Date(ticks)
}
module.exports.displayDate = function(ticks) {
var d = new Date(ticks);
var m_names = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");
return m_names[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
}
module.exports.displayTime = function(ticks) {
var d = new Date(ticks);
var hours = d.getHours();
var minutes = d.getMinutes();
var ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12;
hours = hours ? hours : "12"
minutes = minutes > 9 ? minutes : '0'+minutes
return hours + ":" + minutes + " " + ampm
}
module.exports.displayDateTime = function(ticks) {
var seconds = Math.floor((Date.now() - ticks) / 1000)
interval = Math.floor(seconds / 3600);
if (interval > 24) {
return this.displayTime(ticks)
}
if (interval > 1) {
return interval + " hours ago";
}
if (interval == 1) {
return interval + " hour ago";
}
interval = Math.floor(seconds / 60);
if (interval > 1) {
return interval + " minutes ago";
}
return "1 minute ago";
}
// returns Unix timestamp in milliseconds
module.exports.getTimestamp = function() {
return Date.now();
}
module.exports.extractLinks = function(text) {
var repRegex = new RegExp("<br>", "g");
var linkMatcher = new LinkifyIt();
var matches = linkMatcher.match(text.replace(repRegex, "\n"));
if (!matches) return { "links": null, "text": text };
var links = []
for (var i = 0; i < matches.length; i++) {
links.push(matches[i].url)
}
return { "links": links, "text": text };
}
module.exports.escapeRegExp = function(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
module.exports.getEmbed = function(link) {
var ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
var match = link.trim().match(ytRegex);
if (match && match[1].length==11){
return getYoutubeEmbed(link);
}
// Generl embed feature turned off for now
return;
var id = parseInt((Math.random() * 1000000) + 1);
$.ajax({
type: 'GET',
url: "https://query.yahooapis.com/v1/public/yql",
data: {
q: "select * from html where url=\""+link+"\" and xpath='html/head'",
format: "json"
},
async: true
}).done(function(data) {
if(!data.query.results) {
return;
}
var headerData = data.query.results.head;
var description = ""
for(var i = 0; i < headerData.meta.length; i++) {
if(headerData.meta[i].name && (headerData.meta[i].name === "description" || headerData.meta[i].name === "Description")){
description = headerData.meta[i].content;
break;
}
}
$('.embed-title.'+id).html(headerData.title);
$('.embed-description.'+id).html(description);
})
return (
<div className="post-comment">
<div className={"web-embed-data"}>
<p className={"embed-title " + id} />
<p className={"embed-description " + id} />
<p className={"embed-link " + id}>{link}</p>
</div>
</div>
);
}
var getYoutubeEmbed = function(link) {
var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
var youtubeId = link.trim().match(regex)[1];
var onclick = function(e) {
var div = $(e.target).closest('.video-thumbnail__container')[0];
var iframe = document.createElement("iframe");
iframe.setAttribute("src",
"https://www.youtube.com/embed/" + div.id
+ "?autoplay=1&autohide=1&border=0&wmode=opaque&enablejsapi=1");
iframe.setAttribute("width", "480px");
iframe.setAttribute("height", "360px");
iframe.setAttribute("type", "text/html");
iframe.setAttribute("frameborder", "0");
div.parentNode.replaceChild(iframe, div);
};
var success = function(data) {
$('.video-uploader.'+youtubeId).html(data.data.uploader);
$('.video-title.'+youtubeId).find('a').html(data.data.title);
$(".post-list-holder-by-time").scrollTop($(".post-list-holder-by-time")[0].scrollHeight);
$(".post-list-holder-by-time").perfectScrollbar('update');
};
$.ajax({
async: true,
url: 'https://gdata.youtube.com/feeds/api/videos/'+youtubeId+'?v=2&alt=jsonc',
type: 'GET',
success: success
});
return (
<div className="post-comment">
<h4 className="video-type">YouTube</h4>
<h4 className={"video-uploader "+youtubeId}></h4>
<h4 className={"video-title "+youtubeId}><a href={link}></a></h4>
<div className="video-div embed-responsive-item" id={youtubeId} onClick={onclick}>
<div className="embed-responsive embed-responsive-4by3 video-div__placeholder">
<div id={youtubeId} className="video-thumbnail__container">
<img className="video-thumbnail" src={"https://i.ytimg.com/vi/" + youtubeId + "/hqdefault.jpg"}/>
<div className="block">
<span className="play-button"><span></span></span>
</div>
</div>
</div>
</div>
</div>
);
}
module.exports.areStatesEqual = function(state1, state2) {
return JSON.stringify(state1) === JSON.stringify(state2);
}
module.exports.replaceHtmlEntities = function(text) {
var tagsToReplace = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>'
};
for (var tag in tagsToReplace) {
var regex = new RegExp(tag, "g");
text = text.replace(regex, tagsToReplace[tag]);
}
return text;
}
module.exports.insertHtmlEntities = function(text) {
var tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
for (var tag in tagsToReplace) {
var regex = new RegExp(tag, "g");
text = text.replace(regex, tagsToReplace[tag]);
}
return text;
}
module.exports.searchForTerm = function(term) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH_TERM,
term: term,
do_search: true
});
}
var oldExplicitMentionRegex = /(?:<mention>)([\s\S]*?)(?:<\/mention>)/g;
var puncStartRegex = /^((?![@#])\W)+/g;
var puncEndRegex = /(\W)+$/g;
module.exports.textToJsx = function(text, options) {
if (options && options['singleline']) {
var repRegex = new RegExp("\n", "g");
text = text.replace(repRegex, " ");
} else {
var repRegex = new RegExp("\n", "g");
text = text.replace(repRegex, "<br>");
}
var searchTerm = ""
if (options && options['searchTerm']) {
searchTerm = options['searchTerm'].toLowerCase()
}
var mentionClass = "mention-highlight";
if (options && options['noMentionHighlight']) {
mentionClass = "";
}
var inner = [];
// Function specific regexes
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$/g;
var implicitKeywords = {};
var keywordArray = UserStore.getCurrentMentionKeys();
for (var i = 0; i < keywordArray.length; i++) {
implicitKeywords[keywordArray[i]] = true;
}
var lines = text.split("<br>");
var urlMatcher = new LinkifyIt();
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var words = line.split(" ");
var highlightSearchClass = "";
for (var z = 0; z < words.length; z++) {
var word = words[z];
var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
var explicitMention = mentionRegex.exec(trimWord);
if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != "") {
highlightSearchClass = " search-highlight";
}
if (explicitMention && UserStore.getProfileByUsername(explicitMention[1])) {
var name = explicitMention[1];
// do both a non-case sensitive and case senstive check
var mClass = (name.toLowerCase() in implicitKeywords || name in implicitKeywords) ? mentionClass : "";
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
if (searchTerm === name) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function() {module.exports.searchForTerm(name);}}>@{name}</a>{suffix} </span>);
} else if (urlMatcher.test(word)) {
var match = urlMatcher.match(word)[0];
var link = match.url;
var prefix = word.substring(0,word.indexOf(match.raw))
var suffix = word.substring(word.indexOf(match.raw)+match.raw.length);
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_link"} className={"theme" + highlightSearchClass} target="_blank" href={link}>{match.raw}</a>{suffix} </span>);
} else if (trimWord.match(hashRegex)) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
var mClass = trimWord in implicitKeywords ? mentionClass : "";
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
} else if (trimWord in implicitKeywords) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
if (trimWord.charAt(0) === '@') {
if (searchTerm === trimWord.substring(1).toLowerCase()) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={word+i+z+"_span"} key={name+i+z+"_span"}>{prefix}<a className={mentionClass + highlightSearchClass} key={name+i+z+"_link"} href="#">{trimWord}</a>{suffix} </span>);
} else {
inner.push(<span key={word+i+z+"_span"}>{prefix}<span className={mentionClass + highlightSearchClass}>{module.exports.replaceHtmlEntities(trimWord)}</span>{suffix} </span>);
}
} else if (word === "") {
// if word is empty dont include a span
} else {
inner.push(<span key={word+i+z+"_span"}><span className={highlightSearchClass}>{module.exports.replaceHtmlEntities(word)}</span> </span>);
}
highlightSearchClass = "";
}
if (i != lines.length-1)
inner.push(<br key={"br_"+i+z}/>);
}
return inner;
}
module.exports.getFileType = function(ext) {
ext = ext.toLowerCase();
if (Constants.IMAGE_TYPES.indexOf(ext) > -1) {
return "image";
}
if (Constants.AUDIO_TYPES.indexOf(ext) > -1) {
return "audio";
}
if (Constants.VIDEO_TYPES.indexOf(ext) > -1) {
return "video";
}
if (Constants.SPREADSHEET_TYPES.indexOf(ext) > -1) {
return "spreadsheet";
}
if (Constants.CODE_TYPES.indexOf(ext) > -1) {
return "code";
}
if (Constants.WORD_TYPES.indexOf(ext) > -1) {
return "word";
}
if (Constants.EXCEL_TYPES.indexOf(ext) > -1) {
return "excel";
}
if (Constants.PDF_TYPES.indexOf(ext) > -1) {
return "pdf";
}
if (Constants.PATCH_TYPES.indexOf(ext) > -1) {
return "patch";
}
return "other";
};
module.exports.getIconClassName = function(fileType) {
fileType = fileType.toLowerCase();
if (fileType in Constants.ICON_FROM_TYPE)
return Constants.ICON_FROM_TYPE[fileType];
return "glyphicon-file";
}
module.exports.splitFileLocation = function(fileLocation) {
var fileSplit = fileLocation.split('.');
if (fileSplit.length < 2) return {};
var ext = fileSplit[fileSplit.length-1];
fileSplit.splice(fileSplit.length-1,1)
var filePath = fileSplit.join('.');
var filename = filePath.split('/')[filePath.split('/').length-1];
return {'ext': ext, 'name': filename, 'path': filePath};
}
module.exports.toTitleCase = function(str) {
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
module.exports.changeCss = function(className, classValue) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');
if (cssMainContainer.length == 0) {
var cssMainContainer = $('<div id="css-modifier-container"></div>');
cssMainContainer.hide();
cssMainContainer.appendTo($('body'));
}
// and we need one div for each class
classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
if (classContainer.length == 0) {
classContainer = $('<div data-class="' + className + '"></div>');
classContainer.appendTo(cssMainContainer);
}
// append additional style
classContainer.html('<style>' + className + ' {' + classValue + '}</style>');
}
module.exports.rgb2hex = function(rgb) {
if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb;
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x) {
return ("0" + parseInt(x).toString(16)).slice(-2);
}
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
module.exports.placeCaretAtEnd = function(el) {
el.focus();
if (typeof window.getSelection != "undefined"
&& typeof document.createRange != "undefined") {
var range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (typeof document.body.createTextRange != "undefined") {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(false);
textRange.select();
}
}
module.exports.getCaretPosition = function(el) {
if (el.selectionStart) {
return el.selectionStart;
} else if (document.selection) {
el.focus();
var r = document.selection.createRange();
if (r == null) {
return 0;
}
var re = el.createTextRange(),
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);
return rc.text.length;
}
return 0;
}
module.exports.setSelectionRange = function(input, selectionStart, selectionEnd) {
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(selectionStart, selectionEnd);
}
else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', selectionEnd);
range.moveStart('character', selectionStart);
range.select();
}
}
module.exports.setCaretPosition = function (input, pos) {
module.exports.setSelectionRange(input, pos, pos);
}
module.exports.getSelectedText = function(input) {
var selectedText;
if (document.selection != undefined) {
input.focus();
var sel = document.selection.createRange();
selectedText = sel.text;
} else if (input.selectionStart != undefined) {
var startPos = input.selectionStart;
var endPos = input.selectionEnd;
selectedText = input.value.substring(startPos, endPos)
}
return selectedText;
}
module.exports.isValidUsername = function (name) {
var error = ""
if (!name) {
error = "This field is required";
}
else if (name.length < 3 || name.length > 15)
{
error = "Must be between 3 and 15 characters";
}
else if (!/^[a-z0-9\.\-\_]+$/.test(name))
{
error = "Must contain only lowercase letters, numbers, and the symbols '.', '-', and '_'.";
}
else if (!/[a-z]/.test(name.charAt(0)))
{
error = "First character must be a letter.";
}
else
{
var lowerName = name.toLowerCase().trim();
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++)
{
if (lowerName === Constants.RESERVED_USERNAMES[i])
{
error = "Cannot use a reserved word as a username.";
break;
}
}
}
return error;
}
module.exports.switchChannel = function(channel, teammate_name) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: channel.name,
id: channel.id
});
var domain = window.location.href.split('/channels')[0];
history.replaceState('data', '', domain + '/channels/' + channel.name);
if (channel.type === 'D' && teammate_name) {
document.title = teammate_name + " " + document.title.substring(document.title.lastIndexOf("-"));
} else {
document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-"));
}
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.getPosts(true, channel.id);
$('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right');
client.trackPage();
return false;
}
module.exports.isMobile = function() {
return screen.width <= 768;
}
module.exports.isComment = function(post) {
if ('root_id' in post) {
return post.root_id != "";
}
return false;
}
Image.prototype.load = function(url, progressCallback) {
var thisImg = this;
var xmlHTTP = new XMLHttpRequest();
xmlHTTP.open('GET', url, true);
xmlHTTP.responseType = 'arraybuffer';
xmlHTTP.onload = function(e) {
var h = xmlHTTP.getAllResponseHeaders(),
m = h.match( /^Content-Type\:\s*(.*?)$/mi ),
mimeType = m[ 1 ] || 'image/png';
var blob = new Blob([this.response], { type: mimeType });
thisImg.src = window.URL.createObjectURL(blob);
};
xmlHTTP.onprogress = function(e) {
parseInt(thisImg.completedPercentage = (e.loaded / e.total) * 100);
if (progressCallback) progressCallback();
};
xmlHTTP.onloadstart = function() {
thisImg.completedPercentage = 0;
};
xmlHTTP.send();
};
Image.prototype.completedPercentage = 0;

20
web/sass-files/config.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
require 'compass/import-once/activate'
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "../static/css"
sass_dir = "sass"
images_dir = "../static/img"
javascripts_dir = "../static/js"
fonts_dir = "../static/fonts"
output_style = :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
line_comments = false
color_output = false
preferred_syntax = :scss

154
web/sass-files/sass/partials/_base.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,154 @@
html, body {
height: 100%;
}
body {
font-family: 'Lato', sans-serif;
-webkit-font-smoothing: antialiased;
background: #e9e9e9;
position: relative;
height: 100%;
&.white {
background: #fff;
.inner__wrap {
height: 100%;
}
.row.content {
min-height: 100%;
height: auto !important;
height: 100%;
margin-bottom: -89px;
}
}
> .container-fluid {
@include clearfix;
position: relative;
}
}
a {
word-break: break-word;
}
a.theme {
color: $primary-color;
}
div.theme {
background-color: $primary-color;
}
.nopadding {
padding: 0;
margin: 0;
}
.form-control {
@include border-radius(2px);
}
.form-group {
&.form-group--small {
margin-bottom: 10px;
}
}
.error-panel {
max-width: 275px;
position: absolute;
right: 10px;
top: 40px;
z-index: 100;
}
.btn {
@include single-transition(all, 0.25s, ease-in);
@include border-radius(1px);
&.btn-primary {
border-color: transparent;
background: $primary-color;
&:hover, &:focus, &:active {
background: $primary-color--hover;
}
}
&.btn-inactive {
border-color: transparent;
background: #707070;
color: #fff;
}
}
.relative-div {
position:relative;
}
.command-box {
position:absolute;
background-color:#fff;
width:100%;
border:1px solid #ddd;
bottom: 38;
}
.command-name {
position: relative;
width: 100%;
background-color: #fff;
height: 37px;
line-height: 37px;
padding: 2px 10px 2px 5px;
z-index: 101;
}
.command-name:hover {
background-color:#e8eaed;
}
.command-desc {
color: #a7a8ab;
}
@-webkit-keyframes spin2 {
from { -webkit-transform: rotate(0deg);}
to { -webkit-transform: rotate(360deg);}
}
@keyframes spin {
from { transform: scale(1) rotate(0deg);}
to { transform: scale(1) rotate(360deg);}
}
.black-bg {
background-color: black !important;
}
#error_bar {
background-color: #0099FF;
text-align:center;
position: relative;
color: #fff;
position: fixed;
top: 0;
width: 100%;
z-index: 9999;
.error-bar {
padding: 5px 30px;
}
.error-close {
position: absolute;
right: 0;
top: 0;
color: #FFF;
font-size: 20px;
font-weight: bold;
text-decoration: none;
padding: 0 10px;
}
}
.invite {
width: 90%;
}

63
web/sass-files/sass/partials/_content.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,63 @@
.inner__wrap {
@include single-transition(transform, 0.5s, ease);
&.move--left {
margin-right: 400px;
.search-bar__container {
display: none;
}
}
&.move--right {
.search-bar__container {
display: none;
}
}
}
.app__content {
height: 100%;
padding-top: 50px;
margin-left: 220px;
position: relative;
background: #fff;
.channel__wrap & {
padding-top: 0;
}
}
#post-create {
background: #fff;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}
.post-list {
.new-messages-hr {
margin-top: 5px;
margin-bottom: 0px;
border: 0;
border-top: 1px solid #FF8800;
}
.new-messages-text {
margin-top: 2px;
margin-bottom: 5px;
color: #FF8800;
text-align: center;
}
}
.new-messages-hr {
margin-top: 5px;
margin-bottom: 0px;
border: 0;
border-top: 1px solid #FF8800;
}
.new-messages-text {
margin-top: 2px;
margin-bottom: 5px;
color: #FF8800;
text-align: center;
}
.delete-message-text {
margin-top: 10px;
}

29
web/sass-files/sass/partials/_error.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
body {
&.error {
.container-fluid {
display: table;
height: 90%;
}
.error__container {
max-width: 800px;
margin: 0 auto;
display: table-cell;
vertical-align: top;
padding: 5em 0;
text-align: left;
color: #555;
}
.error__icon {
font-size: 4em;
color: #CCC;
}
h2 {
font-weight: 600;
font-size: 1.5em;
margin: 0.8em 0 0.5em;
}
p {
font-size: 1.2em;
}
}
}

144
web/sass-files/sass/partials/_files.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,144 @@
.preview-container {
position: relative;
width: 100%;
max-height: 110px;
height: 110px;
white-space: nowrap;
overflow: auto;
.preview-div {
display: inline-block;
width: 120px;
height: 100px;
margin: 7px 10px 0 0;
vertical-align: top;
position: relative;
border: 1px solid #DDD;
&.custom-file {
width: 85px;
height: 100px;
}
&:hover .remove-preview:after {
@include opacity(1);
}
.spinner {
position:absolute;
top:50%;
left:50%;
margin-left: -16px;
margin-top: -16px;
width:32px;
height:32px;
}
}
.preview-img {
width:100%;
height:100%;
}
.remove-preview {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
&:after {
background: rgba(0, 0, 0, 0.4);
content: '';
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100%;
@include opacity(0);
}
i {
top: 5px;
right: 5px;
position: absolute;
color: #fff;
cursor: pointer;
z-index: 5;
opacity: inherit;
}
}
}
.image-comment {
background-position:left top;
background-repeat:no-repeat;
width:300px;
height:300px;
}
.file-icon {
width: 100%;
height: 100%;
&.audio {
background: url("../images/icons/audio.png");
@include background-size(100% 100%);
}
&.video {
background: url("../images/icons/video.png");
@include background-size(100% 100%);
}
&.ppt {
background: url("../images/icons/ppt.png");
@include background-size(100% 100%);
}
&.generic {
background: url("../images/icons/generic.png");
@include background-size(100% 100%);
}
&.code {
background: url("../images/icons/code.png");
@include background-size(100% 100%);
}
&.excel {
background: url("../images/icons/excel.png");
@include background-size(100% 100%);
}
&.word {
background: url("../images/icons/word.png");
@include background-size(100% 100%);
}
&.pdf {
background: url("../images/icons/pdf.png");
@include background-size(100% 100%);
}
&.patch {
background: url("../images/icons/patch.png");
@include background-size(100% 100%);
}
&.image {
background: url("../images/icons/image.png");
@include background-size(100% 100%);
}
}
.post-image__column {
position: relative;
width: 120px;
height: 100px;
float: left;
margin: 0 1em 1em 0;
&.custom-file {
width: 85px;
height: 100px;
}
.post__load {
height: 100%;
width: 100%;
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: center;
}
.post__image {
height: 100%;
width: 100%;
border: 1px solid #E2E2E2;
background-color: #FFF;
background-repeat: no-repeat;
}
a {
text-decoration: none;
color: grey;
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

23
web/sass-files/sass/partials/_footer.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
.footer-pane {
background: #eee;
padding-bottom: 1em;
.footer-link {
padding: 0 1.5em;
&.copyright {
color: #999;
padding-right: 0;
}
}
.footer-site-name {
padding: 1.5em 0 1em;
font-size:14px;
font-weight:bold;
text-transform:uppercase;
}
}
.footer, .footer-pane, .footer-push {
height: 89px;
}

215
web/sass-files/sass/partials/_headers.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,215 @@
.row {
&.header {
position: relative;
.channel__wrap & {
display: none;
}
}
}
.header-dropdown__icon {
font-size: 11px;
color: inherit;
top: 3px;
}
.channel-header__info {
display: table;
table-layout: fixed;
width: 100%;
> div {
display: block;
white-space: nowrap;
word-break: break-word;
&.dropdown {
max-width: 100%;
padding-right: 1em;
float: left;
.header-dropdown__icon {
color: #777;
}
a {
text-decoration: none;
}
.modal {
white-space: normal;
}
}
&.description {
overflow: hidden;
text-overflow: ellipsis;
color: #888;
margin-top: 2px;
}
&.popover {
white-space: normal;
}
}
}
.channel-intro {
padding-bottom:5px;
margin: 0 1em 35px;
border-bottom: 1px solid lightgrey;
.intro-links {
margin: 0.5em 1.5em 0 0;
display: inline-block;
.fa {
margin-right: 5px;
}
}
.channel-intro-profile {
margin-top:5px;
margin-left: 63px;
}
.channel-intro-img {
float:left;
}
.channel-intro-title {
font-weight:bold;
}
.channel-intro-text {
margin-top:35px;
}
}
// Team Header in Sidebar
.sidebar--left, .sidebar--menu {
.team__header {
padding: 0 15px 0 15px;
@include legacy-pie-clearfix;
a {
color: #fff;
}
.navbar-right {
font-size: 0.85em;
margin: 16px -5px 0;
.dropdown-toggle {
padding: 0 10px;
}
.dropdown-menu {
li a {
padding: 3 20px;
color: #555;
}
}
.dropdown__icon {
background: url("../images/dropdown-icon.png");
width: 4px;
height: 16px;
@include background-size(100% 100%);
display: inline-block;
}
}
.team__name {
float: left;
line-height: 50px;
font-weight: 600;
font-size: 1.2em;
max-width: 80%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-decoration: none;
}
> .nav {
> li {
> a {
float: right;
background: none !important;
padding: 2px;
&.dropdown-toggle {
line-height: 1.8;
font-size: 1em;
color: #fff;
}
}
}
}
}
}
#navbar {
.navbar-default {
.navbar-toggle {
&.menu-toggle {
padding: 0 1em;
}
}
}
}
.channel-header {
width:100%;
border-left: none;
font-size: 14px;
line-height: 50px;
#member_popover {
width: 45px;
color: #999;
}
&.alt {
margin: 0;
th {
font-weight: normal !important;
}
td {
border: none;
}
}
th {
text-align: center;
&:first-child {
text-align: left !important;
border-left: none;
width: 100%;
padding-left: 1em;
}
&:last-child {
width: 8.9%;
}
}
td {
padding: 5px 25px 5px !important;
font-size: 13px;
text-align: center !important;
&:first-child {
text-align: left !important;
}
}
.heading {
margin: 0;
color: #555;
font-size: 1.3em;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
}
.caret {
margin-top: 3px;
}
.more {
color: #81848b;
display: inline-block;
vertical-align: top;
text-transform: capitalize;
font-size: 13px;
}
.disabled {
color: #999;
}
}
.channel-header__navbar {
font-size: 16px;
.heading {
margin-right: 3px;
}
.header-dropdown__icon {
top: 1px;
}
}

55
web/sass-files/sass/partials/_mentions.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,55 @@
.mention {
color: #fff;
background: $primary-color;
position: relative;
z-index: 10;
padding-bottom: 1px;
@include border-radius(3px);
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.mentions--top {
position: absolute;
z-index:99999;
.mentions-box {
position:absolute;
background-color:#fff;
border:1px solid #ddd;
overflow:scroll;
bottom:0;
}
}
.mentions-name {
position:relative;
width:100%;
background-color:#fff;
height:37px;
padding:2px;
z-index:101;
}
.mentions-name:hover {
background-color:#e8eaed;
}
.mentions-text {
font-color:black;
}
.mention-img {
margin-right:10px;
height:32px;
width:32px;
border-radius: 10%;
}
.mention-highlight {
background-color:#fff2bb;
}
.mention-link {
color:$primary-color;
}

248
web/sass-files/sass/partials/_modal.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,248 @@
.modal {
&.image_modal {
.modal-backdrop.in {
@include opacity(0.7);
}
}
.remove__member {
float: right;
}
.modal-dialog {
max-width: 95%;
margin-left: auto;
margin-right: auto;
}
.modal-body {
max-height: 75%;
overflow: auto;
}
.modal-push-down {
margin-top: 5%;
}
.modal-next-bar {
position:absolute;
top:0px;
right:0;
height:100%;
}
.modal-header {
border-radius: 0;
background: $primary-color;
color: #FFF;
padding: 15px 15px 11px;
border: none;
min-height: 56px;
@include clearfix;
.modal-title {
float: left;
}
.modal-action {
padding: 0;
margin: 0;
}
button.close {
margin-top: 0;
color: #fff;
@include opacity(1);
z-index: 5;
position: relative;
}
.btn {
margin-right: 10px;
&.btn-primary {
float: right;
margin-top: -4px;
position: relative;
i {
margin-right: 5px;
}
}
}
}
.no-channel-message {
text-align: center;
padding: 2em 1em;
.primary-message {
font-size: 1.2em;
}
.secondary-message {
font-size: 1.25em;
color: #888;
margin: 1em 0 0;
}
}
.modal-content {
border-radius: 0;
box-shadow: none;
}
.modal-chevron-icon {
top:50%;
font-size:120%;
}
.modal-prev-bar {
position:absolute;
top:0px;
left:0;
height:100%;
}
&#more_channels {
.modal-body {
padding: 5px 10px;
}
}
.more-channel-table {
margin: 0;
table-layout: fixed;
p {
font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
color: #999;
margin: 5px 0;
}
.more-channel-name {
color: #444;
font-weight: bold;
font-size: 0.95em;
}
tbody {
> tr {
&:first-child {
td {
border: none;
}
}
td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.td--action {
width: 70px;
vertical-align: middle;
}
}
}
}
}
.modal-image {
position:relative;
width:100%;
margin: 0 auto;
.image-wrapper {
padding: 4px;
background: #FFF;
position: relative;
max-width: 80%;
min-height: 280px;
min-width: 280px;
@include border-radius(4px);
display: table;
margin: 0 auto;
&:hover .modal-close {
@include opacity(1);
}
&.default {
width: 100%;
height: 80%;
}
.modal-close {
background: url("../images/close.png") no-repeat;
@include background-size(100% 100%);
width: 37px;
height: 37px;
position: absolute;
right: -13px;
top: -13px;
@include opacity(0);
cursor: pointer;
z-index: 9999;
}
> a {
display: table-cell;
vertical-align: middle;
}
img {
max-width: 100%;
max-height: 100%;
}
}
.image-content {
box-shadow: none;
background: rgba(0, 0, 0, 0);
width: 100%;
height: 100%;
padding: 0;
border: none;
display: table;
}
.image-body {
vertical-align: middle;
display: table-cell;
text-align: center;
height: 100%;
padding: 0;
position: relative;
overflow: visible;
}
.image-control {
width: 50px;
height: 45px;
float: left;
background: url(../images/prev.png) left no-repeat;
top: 50%;
position: relative;
margin-top: -23px;
&.image-next {
float: right;
background: url(../images/next.png) left no-repeat;
}
}
.loader-image {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.loader-percent {
position: absolute;
top: 55px;
bottom: 0;
left: 0;
right: 0;
margin: auto;
color:grey;
height:20px;
}
.modal-button-bar {
position:absolute;
bottom:0px;
left:0px;
right:0px;
background-color:rgba(0, 0, 0, 0.8);
@include opacity(0);
-webkit-transition: opacity 0.6s;
-moz-transition: opacity 0.6s;
-o-transition: opacity 0.6s;
transition: opacity 0.6s;
line-height: 40px;
padding: 0 10px;
margin: 4px;
&.footer--show {
@include opacity(1);
}
.image-links {
a, span {
float: right;
}
}
.text {
vertical-align:middle;
bottom:0;
color:white;
margin-left:5px;
}
}
}
}

101
web/sass-files/sass/partials/_navbar.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,101 @@
#navbar{
input {
margin: 0px 5px 0px 2px;
}
.navbar-default {
position: absolute;
border: none;
min-height: 50px;
background: $primary-color;
.navbar-nav {
> li {
> a {
height: 50px;
padding: 0 1.3em;
i {
line-height: 50px;
}
}
}
}
.navbar-toggle {
float: left;
border-color: transparent;
border-radius: 0;
margin: 0;
padding: 0 10px;
line-height: 50px;
height: 50px;
z-index: 5;
.icon-bar {
background: #fff;
width: 21px;
}
&:hover, &:active, &:focus {
background: inherit;
}
}
.navbar-brand {
padding: 0 0.5em;
height: 50px;
line-height: 50px;
float: none;
font-size: 16px;
.heading {
margin-right: 3px;
color: #fff;
}
.header-dropdown__icon {
top: 1px;
}
.dropdown-toggle {
color: #fff;
}
.description {
display: inline-block;
margin-right: 0.5em;
color: #fff;
&.info-popover {
width: 19px;
height: 19px;
background: url("../images/info__icon.png");
@include background-size(100% 100%);
vertical-align: middle;
top: -1px;
position: relative;
cursor: pointer;
}
}
}
}
.sidebar-channel {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
span {
white-space: nowrap;
margin-left: 2px;
}
}
.channel-create-btn {
margin-right: 15px;
}
.more-channel-table {
width: 100%;
border: 1px solid #dbdbdc;
td {
padding: 7px;
}
button {
width: 110px;
}
}
}
.badge-notify {
background:red;
position: absolute;
right: -5px;
top: -5px;
z-index: 100;
}

120
web/sass-files/sass/partials/_perfect-scrollbar.scss Исполняемый файл
Просмотреть файл

@@ -0,0 +1,120 @@
.ps-container {
overflow: hidden !important; }
.ps-container.ps-active-x > .ps-scrollbar-x-rail, .ps-container.ps-active-y > .ps-scrollbar-y-rail {
display: block; }
.ps-container.ps-in-scrolling {
pointer-events: none; }
.ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x {
background-color: #999; }
.ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y {
background-color: #999; }
.ps-container > .ps-scrollbar-x-rail {
display: none;
position: absolute;
/* please don't change 'position' */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
-webkit-transition: background-color .2s linear, opacity .2s linear;
-moz-transition: background-color .2s linear, opacity .2s linear;
-o-transition: background-color .2s linear, opacity .2s linear;
transition: background-color .2s linear, opacity .2s linear;
bottom: 3px;
/* there must be 'bottom' for ps-scrollbar-x-rail */
height: 8px; }
.ps-container > .ps-scrollbar-x-rail > .ps-scrollbar-x {
position: absolute;
/* please don't change 'position' */
background-color: #aaa;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
-webkit-transition: background-color .2s linear;
-moz-transition: background-color .2s linear;
-o-transition: background-color .2s linear;
transition: background-color .2s linear;
bottom: 0;
/* there must be 'bottom' for ps-scrollbar-x */
height: 8px; }
.ps-container > .ps-scrollbar-y-rail {
display: none;
position: absolute;
/* please don't change 'position' */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
-webkit-transition: background-color .2s linear, opacity .2s linear;
-moz-transition: background-color .2s linear, opacity .2s linear;
-o-transition: background-color .2s linear, opacity .2s linear;
transition: background-color .2s linear, opacity .2s linear;
right: 3px;
/* there must be 'right' for ps-scrollbar-y-rail */
width: 8px; }
.ps-container > .ps-scrollbar-y-rail > .ps-scrollbar-y {
position: absolute;
/* please don't change 'position' */
background-color: #aaa;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
-webkit-transition: background-color .2s linear;
-moz-transition: background-color .2s linear;
-o-transition: background-color .2s linear;
transition: background-color .2s linear;
right: 0;
/* there must be 'right' for ps-scrollbar-y */
width: 8px; }
.ps-container:hover.ps-in-scrolling {
pointer-events: none; }
.ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x {
background-color: #999; }
.ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y {
background-color: #999; }
.ps-container:hover > .ps-scrollbar-x-rail, .ps-container:hover > .ps-scrollbar-y-rail {
opacity: 0.6;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)";
filter: alpha(opacity=60); }
.ps-container:hover > .ps-scrollbar-x-rail:hover {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container:hover > .ps-scrollbar-x-rail:hover > .ps-scrollbar-x {
background-color: #999; }
.ps-container:hover > .ps-scrollbar-y-rail:hover {
background-color: #eee;
opacity: 0.9;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
filter: alpha(opacity=90); }
.ps-container:hover > .ps-scrollbar-y-rail:hover > .ps-scrollbar-y {
background-color: #999; }

380
web/sass-files/sass/partials/_post.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,380 @@
.custom-textarea {
white-space:pre-wrap;
word-wrap:break-word;
background:transparent;
border:1px solid #ccc !important;
position:absolute;
top:0px;
height:auto;
resize: none;
line-height:20px;
min-height:36px;
}
.textarea-div {
white-space:pre-wrap;
word-wrap:normal;
color:rgba(0,0,0,0);
position:absolute;
top:0px;
word-break: break-word;
left:1px;
line-height:20px;
min-height:36px;
height:auto;
border:0;
}
body.ios {
.textarea-div {
padding: 7px 17px 7px 15px;
-webkit-overflow-scrolling: auto;
}
}
.textarea-div::-webkit-scrollbar {
display:none;
}
.textarea-wrapper {
position:relative;
min-height:37px;
}
#post-list {
.new-seperator {
text-align: center;
padding: 1em 0;
.new-seperator__hr {
border-color: #FFAF53;
margin: 0;
}
.new-seperator__text {
margin-top: -11px;
color: #FF8800;
background: #FFF;
display: inline-block;
padding: 0 1em;
font-weight: normal;
}
}
.date-seperator {
text-align: center;
margin: 1em 0;
height: 0;
&.hovered--after {
margin-bottom: 0;
padding-bottom: 1em;
background: #f6f6f6;
}
&.hovered--before {
margin-top: 0;
padding-top: 1em;
background: #f6f6f6;
}
.date-seperator__hr {
border-color: #ccc;
margin: 0;
}
.date-seperator__text {
margin-top: -13px;
line-height: 24px;
color: #555;
background: #FFF;
display: inline-block;
padding: 0 1em;
font-weight: 900;
@include border-radius(50px);
}
}
.post-list-holder-by-time {
background: #fff;
overflow-y: scroll;
width: 100%;
padding: 1em 0 0;
position: relative;
}
.post-list__table {
display: table;
table-layout: fixed;
width: 100%;
min-height: 100%;
.post-list__content {
display: table-cell;
vertical-align: bottom;
}
}
.more-messages-text {
margin-top: 2px;
margin-bottom: 5px;
display: block;
text-align: center;
outline: none;
border: none;
}
.beginning-messages-text {
margin-top: 2px;
margin-bottom: 5px;
display: block;
text-align: center;
color: grey;
}
}
.post-create__container {
form {
width: 100%;
padding: 0 1em;
margin: 0;
}
.post-create-body {
max-width: 850px;
padding: 0 0 2px;
position: relative;
.custom-textarea {
padding-top: 8px;
padding-right: 28px;
max-height: 160px;
overflow: auto;
line-height: 1.5;
}
.textarea-div {
padding-top: 8px;
padding-right: 30px;
max-height: 160px;
overflow: auto;
line-height: 1.5;
}
.btn-file {
right: 0;
position: absolute;
top: 1px;
color: #999;
@include single-transition(all, 0.15s);
font-size: 16px;
padding: 9px;
&:hover, &:active {
color: #444;
box-shadow: none;
}
}
textarea {
border: none;
box-shadow: none;
}
}
.post-create-footer {
@include clearfix;
padding: 0;
.msg-typing {
min-height: 20px;
line-height: 18px;
display: inline-block;
font-size: 13px;
color: #777;
}
}
}
.post {
word-wrap: break-word;
padding: 0.4em 1em;
position: relative;
max-width: 100%;
@include legacy-pie-clearfix;
&:hover {
.post-header .post-header-col.post-header__reply {
.dropdown, .comment-icon__container {
@include opacity(1);
}
}
background: #f6f6f6;
}
&.post--comment {
&.other--root {
.post-comment {
border-left: 4px solid #EEE;
margin-left: 30px;
padding: 2px 0 2px 10px;
}
}
}
&.same--root {
.comment-icon__container {
@include opacity(0);
}
div.post-profile-img__container {
.post-profile-img {
visibility: hidden;
}
}
.post__content {
padding: 0;
}
.post-body {
border-left: 4px solid #EEE;
width: 570px;
margin-left: 30px;
padding-left: 10px;
.post-link {
display: none;
}
}
}
&.post--last {
.post-header {
.post-header-col.post-header__reply {
.dropdown-menu {
top: auto;
bottom: 25px;
}
}
}
}
.post-create-footer {
padding: 0;
}
p {
margin: 0 0 5px;
white-space: pre-wrap;
}
.comment-icon__container {
fill: $primary-color;
.comment-icon {
display: inline-block;
top: 3px;
position: relative;
margin-right: 3px;
fill: inherit;
}
path {
fill: inherit;
}
}
> div {
&.post-profile-img__container {
float: left;
.post-profile-img {
margin-right: 10px;
@include border-radius(3px);
}
}
&.post__content {
padding-left: 46px;
max-width: 100%;
@include legacy-pie-clearfix;
}
}
.post-image__columns {
@include legacy-pie-clearfix;
margin-top: 1em;
}
.post-info--hidden {
display: none;
}
.post-body {
max-width: 100%;
width: 600px;
float: left;
word-wrap: break-word;
.post-link {
@include clearfix;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
}
.create-reply-form-wrap {
width: 100%;
margin-top: 5px;
form {
padding: 0;
}
.comment-btn {
display: none;
}
}
.post-header {
list-style-type: none;
margin: 0 0 1px;
padding-left: 0px;
&.post-header-post {
width: 200px;
text-align: right;
float: left;
padding-right: 15px;
}
&.post-info {
.post-profile-time {
width: 100px;
display: inline-block;
margin-left: 50px;
}
}
.post-header-col {
vertical-align: top;
display: inline-block;
margin-right: 10px;
&.post-header__reply {
min-width: 70px;
.dropdown-menu {
right: 0;
left: auto;
}
.dropdown {
min-width: 18px;
margin-right: 7px;
display: inline-block;
@include opacity(0);
}
}
}
.post-profile-time {
color: #a8adb7;
}
}
.post-comment {
}
.web-embed-data {
padding: 2px 0 0 10px;
background: #f9f9f9;
background: rgba(0, 0, 0, 0.05);
@include border-radius(2px);
height: 50px;
overflow: hidden;
text-overflow: ellipsis;
.embed-title {
margin: 3px 0 1px;
color: #555;
font-weight: bold;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.embed-description {
margin: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: #888;
}
.embed-link {
display: none;
}
}
.msg-typing {
margin-top: 2px;
margin-left: 5px;
color: #a8adb7;
}
.post-grey {
color:lightgrey;
}
.post-reply {
clear: both;
width: 100%;
word-break: break-word;
.btn-file {
width: 40px;
}
}
}

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

@@ -0,0 +1,77 @@
.post-right__container {
.post-right-root-message {
padding: 1em 1em 0;
}
.post-right-comments-container {
position: relative;
padding: 0;
}
.post {
&.post--root {
padding: 1em;
hr {
border-color: #DDD;
margin: 1em 0 0 0;
}
}
}
.post-create__container {
.custom-textarea {
min-height: 100px;
}
}
}
.post-right-header {
font-size: 1em;
text-transform: uppercase;
color: #999;
font-weight: 400;
color: #888;
height: 39px;
padding: 10px 10px 0 15px;
border-bottom: $border-gray;
}
.post-right-root-container {
padding: 5px 10px;
border-bottom: $border-gray;
}
.post-right-root-container {
ul {
padding-left: 0;
margin-bottom: 2px;
}
}
.post-right-root-container li {
display: inline;
list-style-type: none;
padding-right: 3px;
}
.post-right-root-time {
color: #a8adb7;
}
.post-right-create-comment-container {
padding: 5px;
margin-bottom: 18px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}
.post-right__scroll {
position: relative;
}
.post-right-comment-time {
color: #a8adb7;
}

637
web/sass-files/sass/partials/_responsive.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,637 @@
@media screen and (max-width: 1800px) {
.inner__wrap {
&.move--left {
.post {
&.post--comment {
&.other--root {
.post-comment {
margin-left: 0;
}
}
}
&.same--root {
margin-left: 60px;
padding-left: 10px;
border-left: 4px solid #EEE;
div.post-profile-img__container {
.post-profile-img {
display: none;
}
}
.post__content {
width: 825px;
}
.post-body {
padding-left: 0;
border: none;
margin: 0;
}
}
.post__content {
width: 920px;
}
.post-header {
&.post-header-post {
width: auto;
float: none;
text-align: left;
padding-right: 0;
}
&.post-info {
display: none;
float: none;
}
.post-header-col {
&.post-header__reply {
float: right;
text-align: right;
.reply-link {
margin-right: 0;
}
.dropdown {
margin-left: 10px;
}
}
}
.post-info--hidden {
display: inline;
.post-info {
display: inline;
.post-profile-time {
margin: 0;
}
}
}
}
.post-body {
float: none;
width: 750px;
}
}
}
}
}
@media screen and (max-width: 1440px) {
.post-create__container {
.post-create-body {
max-width: 810px;
}
}
.post {
&.post--comment {
&.other--root {
.post-comment {
margin-left: 0;
}
}
}
&.same--root {
margin-left: 60px;
padding-left: 10px;
border-left: 4px solid #EEE;
div.post-profile-img__container {
.post-profile-img {
display: none;
}
}
.post__content {
width: 825px;
}
.post-body {
padding-left: 0;
border: none;
margin: 0;
}
}
.post__content {
width: 880px;
}
.post-header {
&.post-header-post {
width: auto;
float: none;
text-align: left;
padding-right: 0;
}
&.post-info {
display: none;
float: none;
}
.post-header-col {
&.post-header__reply {
float: right;
margin: 0;
}
}
.post-info--hidden {
display: inline;
.post-info {
display: inline;
.post-profile-time {
width: auto;
margin: 0;
}
}
}
}
.post-body {
float: none;
width: 750px;
}
}
}
@media screen and (max-width: 1140px) {
.post {
.post__content {
width: 100%;
}
.post-header {
.post-header-col {
&.post-header__reply {
.reply-link {
margin-right: 0;
}
.dropdown {
margin-left: 10px;
}
}
}
}
}
}
@media screen and (max-width: 960px) {
.post {
&:hover {
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@include opacity(0);
}
}
}
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@include opacity(0);
}
.dropdown {
@include opacity(1);
}
.comment-icon__container__show {
@include opacity(1);
}
}
}
.signup-team__container {
font-size: 1em;
}
.post {
.post-header {
.post-header-col {
&.post-header__reply {
text-align: right;
display: inline-block;
}
}
}
}
}
@media screen and (min-width: 768px) {
.second-bar {
display: none;
}
}
@media screen and (max-width: 768px) {
#post-list {
.date-seperator {
&.hovered--after {
background: none;
}
&.hovered--before {
background: none;
}
}
}
.post {
&.same--root {
margin-left: 25px;
}
&:hover {
background: none;
}
}
.signup-team__container {
padding: 30px 0;
margin-bottom: 30px;
font-size: 0.9em;
.signup-team__name {
font-size: 2em;
}
}
.modal {
.modal-header {
padding-left: 20px;
padding-right: 20px;
.modal-action {
margin-top: 10px;
}
button.close {
width: 35px;
height: 32px;
margin: -5px -10px 0;
}
.modal-title {
float: none;
}
.btn {
&.btn-primary {
margin: 8px 0 0 -10px;
float: none;
}
}
}
}
.settings-modal {
&.display--content {
.modal-header {
display: none;
}
.settings-table {
display: block;
.settings-content {
display: block;
.section-edit {
text-align: left;
}
}
.settings-links {
display: none;
}
.modal-header {
display: block;
position: fixed;
width: 100%;
z-index: 5;
}
.user-settings {
.tab-header {
display: none;
}
.divider-dark.first {
display: none;
}
}
}
}
.settings-table {
.settings-content {
padding: 0;
display: none;
.user-settings {
padding: 70px 20px 30px;
}
}
.settings-links {
background: #fff;
}
.nav {
> li {
> a {
border-top: 1px solid #ddd;
font-size: 1.1em;
line-height: 2.7;
color: #555;
.glyphicon {
margin-left: 7px;
}
}
&.active, &:hover {
a {
color: #555;
background: #fff;
}
}
}
}
}
}
#post-create {
.post-create-body {
padding-bottom: 10px;
}
.post-create-footer .msg-typing {
display: none;
}
}
.preview-container {
.preview-div {
margin-top: 0;
}
.remove-preview {
width: 50px;
height: 50px;
left: 50%;
top: 50%;
background: #444;
margin: -25px 0 0 -25px;
@include border-radius(50px);
text-align: center;
&:after {
display: none;
}
i {
line-height: 50px;
top: auto;
right: auto;
position: relative;
font-size: 28px;
}
}
}
#navbar {
.navbar-default {
.navbar-header {
margin: 0 -15px;
.dropdown__icon {
background: url("../images/dropdown-icon.png");
width: 4px;
height: 16px;
@include background-size(100% 100%);
display: inline-block;
}
}
.navbar-toggle {
display: block;
}
.navbar-brand {
white-space: nowrap;
}
}
}
#user_settings {
border-right: none;
}
body {
&.white {
.row.content {
margin-bottom: -185px;
}
}
}
.footer, .footer-pane, .footer-push {
height: 185px;
}
.footer-pane {
.footer-link {
padding: 0;
width: 100%;
text-align: right;
line-height: 1.7;
&.copyright {
width: 100%;
}
}
}
.search-bar__container {
padding: 10px 8px 13px;
background: $primary-color;
color: #fff;
.search__form {
border: none;
padding: 0 10px 0 30px;
.form-control {
background: none;
color: #fff;
border-bottom: 1px solid #fff;
border-bottom: 1px solid rgba(#fff, 0.7);
border-radius: 0;
padding: 0 0 0 23px;
}
::-webkit-input-placeholder {
color: #fff;
}
:-moz-placeholder { /* Firefox 18- */
color: #fff;
}
::-moz-placeholder { /* Firefox 19+ */
color: #fff;
}
:-ms-input-placeholder {
color: #fff;
}
}
}
.sidebar--menu {
@include single-transition(transform, 0.5s, ease);
@include translate3d(290px, 0, 0);
width: 290px;
border: none;
display: block;
&.move--left {
@include translate3d(0, 0, 0);
}
}
.sidebar--left {
@include single-transition(transform, 0.5s, ease);
@include translate3d(-290px, 0, 0);
width: 290px;
border: none;
&.sidebar--padded {
padding-top: 0;
}
&.move--right {
@include translate3d(0, 0, 0);
}
> div {
padding-bottom: 105px;
}
.team__header {
display: none;
@include clearfix;
}
.search__form {
display: block;
}
.nav {
li {
&.dropdown.open {
padding-bottom: 25px;
ul {
clear: both;
background: #FFF;
border-radius: 3px;
top: 5px;
position: relative;
li {
a {
line-height: 30px;
}
}
}
}
h4 {
margin: 16px 0 8px;
}
> a {
font-size: 15px;
margin: 0;
line-height: 2.5;
&:hover, &:focus {
background: transparent;
}
}
}
}
}
.sidebar--right {
width: 100%;
right: 0;
@include translate3d(100%, 0, 0);
z-index: 5;
&.move--left {
@include translate3d(0, 0, 0);
}
.sidebar__collapse, .sidebar__search-icon {
display: block;
}
.sidebar--right__close {
display: none;
}
}
.inner__wrap {
&.move--right {
@include translate3d(290px, 0, 0);
&:before {
z-index: 9999;
content: "";
width: 100%;
height: 100%;
left: -15px;
top: 0;
position: absolute;
background: rgba(0, 0, 0, 0.4);
}
}
&.move--left-small {
@include translate3d(-290px, 0, 0);
&:before {
z-index: 9999;
content: "";
width: 100%;
height: 100%;
right: -15px;
top: 0;
position: absolute;
background: rgba(0, 0, 0, 0.4);
}
}
&.move--left {
margin: 0;
@include translate3d(-100%, 0, 0);
}
}
.modal {
.modal-image {
.image-wrapper {
padding-bottom: 40px;
.modal-close {
@include opacity(1);
}
}
.modal-button-bar {
@include opacity(1);
}
}
}
.app__content {
padding-top: 50px;
margin: 0;
.channel__wrap & {
padding-top: 50px;
}
.channel-header {
display: none;
}
}
.channel__wrap {
.row {
&.header {
display: block;
}
}
}
.post {
.post-profile-img__container {
display: none;
}
&.post--comment {
&.other--root {
.post-comment {
margin-left: 11px;
}
}
}
> div {
&.post__content {
padding: 0;
}
}
}
.post-comments {
padding: 9px 21px 10px 10px !important;
}
}
@media screen and (max-width: 480px) {
.modal {
.modal-body {
max-height: 70%;
}
.settings-modal {
&.display--content {
.modal-body {
max-height: 90%;
}
}
}
}
.member-div {
padding: 8px 0;
.member-drop, .member-role {
position: relative;
margin: 0px 0 0 44px;
padding: 5px 0;
top: 0;
right: 0;
}
.open>.dropdown-menu {
left: 0;
right: auto;
}
}
.sidebar--left {
@include translate3d(-260px, 0, 0);
width: 260px;
}
.inner__wrap {
&.move--right {
@include translate3d(260px, 0, 0);
}
}
.modal {
.modal-image {
.modal-button-bar {
line-height: 30px;
padding: 5px;
}
}
}
}

95
web/sass-files/sass/partials/_search.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,95 @@
.search-bar__container {
padding: 8px 8px 8px 0;
}
.sidebar__collapse {
width: 20px;
height: 30px;
position: absolute;
top: 10px;
left: 6px;
cursor: pointer;
background: url("../images/arrow-left.png") center no-repeat;
@include background-size(10px 15px);
z-index: 5;
display: none;
}
.sidebar__search-icon {
position: absolute;
left: 40px;
top: 18px;
@include opacity(0.8);
display: none;
}
.search__form {
.search-bar__container & {
margin: 0;
border: 1px solid #DDD;
@include border-radius(2px);
width: 300px;
}
.sidebar--right & {
width: 100%;
}
.search-bar-box {
height: 40px;
border: 1px solid #ddd;
padding-right: 30px;
box-shadow: none;
.search-bar__container & {
height: 30px;
border: none;
}
}
.glyphicon-refresh-animate {
top: 27px;
position: absolute;
right: 27px;
color: #999;
.search-bar__container & {
right: 7px;
top: 8px;
}
}
}
.search-items-container {
position: relative;
}
.search-results-header {
font-size: 1em;
text-transform: uppercase;
color: #999;
font-weight: 400;
color: #888;
height: 44px;
line-height: 44px;
padding: 0 10px 0 10px;
border-bottom: $border-gray;
}
.search-item-container {
border-top: $border-gray;
padding: 10px 1em;
margin: 0;
cursor: pointer;
&:first-child {
border: none;
}
.search-channel__name {
font-weight: bold;
margin: 0 0 10px 0;
}
}
.search-item-time {
color: #a8adb7;
}
.search-results-none {
padding: 10px;
}
.search-highlight.theme, .search-highlight {
background-color: #FFF2BB;
}

222
web/sass-files/sass/partials/_settings.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,222 @@
.user-settings {
background: #fff;
min-height:300px;
}
.settings-modal {
width:800px;
max-width: 100%;
.modal-back {
width: 8px;
height: 13px;
background: url("../images/arrow-left.png");
@include background-size(100% 100%);
margin-right: 10px;
display: inline-block;
cursor: pointer;
}
.modal-body {
padding: 0;
}
li {
list-style: none;
}
.settings-table {
display: table;
width: 100%;
> div {
display: table-cell;
vertical-align: top;
}
.settings-links {
width: 180px;
background: #FAFAFA;
border-right: 1px solid #DDD;
}
.settings-content {
padding: 0px 20px 30px;
.modal-header {
display: none;
}
.section-min {
padding: 1em 0;
margin-bottom: 0;
@include clearfix;
}
.section-max {
background: #f2f2f2;
padding: 1em 0;
margin-bottom: 0;
@include clearfix;
.section-title {
margin-bottom: 10px;
}
}
.section-title {
margin-bottom: 5px;
font-weight: bold;
}
.section-edit {
text-align: right;
margin-bottom: 5px;
}
.section-describe {
color:grey;
}
.divider-dark {
border-bottom:1px solid #aaaaaa;
}
.divider-light {
border-bottom:1px solid lightgrey;
}
.setting-list {
padding: 0;
list-style-type:none;
}
.mentions-input {
margin-top: 10px;
}
.setting-list-item {
margin-top:7px;
.has-error {
color: #a94442;
}
.control-label {
color: #555;
}
hr {
border-color: #ccc;
}
}
}
}
.nav-pills {
> li {
margin: 0;
a {
border-radius: 0;
color: #777;
}
.glyphicon {
width: 25px;
top: 2px;
}
&:hover {
a {
background: #E6F2FA;
}
}
&.active {
a {
color: #111;
background-color: #E1E1E1;
}
}
}
}
h3 {
font-size: em(20px);
}
}
#user_settings {
padding: 0 0.5em;
border-right: 1px solid #ddd;
max-width: 800px;
}
.channel-settings {
padding: 0 10px;
}
.tab-header {
margin-bottom:1em;
}
.setting-name {
color:grey;
font-weight:500;
}
.profile-img {
width:158px;
max-height:128px;
}
.sel-btn {
margin-right:5px;
}
.member-list-holder {
background-color:#fff;
}
.member-div {
border-bottom:1px solid lightgrey;
position:relative;
padding:2px;
margin: 0;
width:100%;
&:first-child {
border-top:1px solid lightgrey;
}
.post-profile-img {
@include border-radius(3px);
margin-right: 8px;
}
.member-name {
font-weight:500;
display: block;
}
.member-email {
color:darkgrey;
display: block;
}
.member-role, .member-drop {
position:absolute;
right:15px;
top:13px;
color:#414142;
}
}
.member-invite {
position:absolute;
right:10px;
top:7px;
}
.member-menu {
right:0px;
left:auto;
}
.member-list {
width:100%;
overflow-x:visible;
}
.member-page {
padding-top:50px;
}
.active-border {
border:1px solid red;
}
.color-btn {
margin:4px;
}

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

@@ -0,0 +1,95 @@
.sidebar--left {
position: absolute;
width: 220px;
left: 0;
height: 100%;
border-right: $border-gray;
padding: 0 0 2em 0;
background: #fafafa;
&.sidebar--padded {
padding-top: 44px;
}
.search__form {
margin: 0;
padding: 1em 1em 0;
display: none;
}
> div {
height: 100%;
position: absolute;
padding-bottom: 70px;
width: 100%;
}
.badge {
background-color: $primary-color;
}
.status {
position:relative;
top:1px;
margin-right: 3px;
.online--icon {
fill: #7DBE00;
}
}
.nav-pills__container {
height: 100%;
position: relative;
overflow: auto;
}
.nav {
&.nav-stacked {
> li+li {
margin: 0;
}
}
li {
> h4 {
font-size: 1em;
text-transform: uppercase;
margin: 1.1em 0 0.5em;
font-weight: 400;
color: #AAA;
letter-spacing: -0.3px;
padding: 0 10px 0 15px;
}
> a {
padding: 3px 10px 3px 25px;
line-height: 1.5;
border-radius: 0;
color: #999;
&.unread-title {
color: #333;
font-weight: bold;
}
&:hover, &:focus {
background: #e6f2fa;
}
}
&.active {
a, a:hover, a:focus {
color: #111;
background-color: #e1e1e1;
border-radius: 0;
}
}
}
}
.modal-title {
line-height: 2em;
}
.add-channel-btn {
float: right;
outline: none;
margin: -2px 0 0 0;
color: #AAA;
padding: 0 5px;
text-decoration: none;
font-size: 22px;
line-height: 18px;
font-weight: 700;
&:hover {
color: #666;
}
}
}

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

@@ -0,0 +1,66 @@
.sidebar--menu {
position: absolute;
width: 220px;
right: 0;
height: 100%;
border-right: $border-gray;
padding: 0 0 2em 0;
background: #fafafa;
display: none;
.nav-pills__container {
padding-top: 5px;
}
.team__header {
display: none;
padding: 0 15px;
@include legacy-pie-clearfix;
a {
color: #fff;
}
.navbar-right {
font-size: 0.85em;
margin: 16px -5px 0;
.dropdown-toggle {
padding: 0 10px;
}
.dropdown-menu {
li a {
padding: 3 20px;
color: #555;
}
}
.dropdown__icon {
background: url("../images/dropdown-icon.png");
width: 4px;
height: 16px;
@include background-size(100% 100%);
display: inline-block;
}
}
.team__name {
float: left;
line-height: 50px;
font-weight: 600;
font-size: 1.2em;
max-width: 80%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-decoration: none;
}
}
.nav {
> li {
> a {
font-size: 15px;
background: none !important;
color: #444;
line-height: 40px;
padding: 0 15px;
.glyphicon {
width: 25px;
}
}
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше