From 56e74239d6b34df8f30ef046f0b0ff4ff0866a71 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sun, 14 Jun 2015 23:53:32 -0800 Subject: first commit --- web/react/components/channel_header.jsx | 249 +++++ web/react/components/channel_info_modal.jsx | 50 + web/react/components/channel_invite_modal.jsx | 157 ++++ web/react/components/channel_loader.jsx | 62 ++ web/react/components/channel_members.jsx | 154 ++++ web/react/components/channel_notifications.jsx | 120 +++ web/react/components/command_list.jsx | 67 ++ web/react/components/create_comment.jsx | 166 ++++ web/react/components/create_post.jsx | 273 ++++++ web/react/components/delete_channel_modal.jsx | 58 ++ web/react/components/delete_post_modal.jsx | 108 +++ web/react/components/edit_channel_modal.jsx | 57 ++ web/react/components/edit_post_modal.jsx | 100 ++ web/react/components/email_verify.jsx | 35 + web/react/components/error_bar.jsx | 69 ++ web/react/components/file_preview.jsx | 54 ++ web/react/components/file_upload.jsx | 129 +++ web/react/components/find_team.jsx | 72 ++ web/react/components/get_link_modal.jsx | 55 ++ web/react/components/invite_member_modal.jsx | 179 ++++ web/react/components/login.jsx | 197 ++++ web/react/components/member_list.jsx | 34 + web/react/components/member_list_item.jsx | 67 ++ web/react/components/member_list_team.jsx | 120 +++ web/react/components/mention.jsx | 16 + web/react/components/mention_list.jsx | 127 +++ web/react/components/message_wrapper.jsx | 17 + web/react/components/more_channels.jsx | 109 +++ web/react/components/more_direct_channels.jsx | 68 ++ web/react/components/msg_typing.jsx | 49 + web/react/components/navbar.jsx | 351 ++++++++ web/react/components/new_channel.jsx | 139 +++ web/react/components/password_reset.jsx | 178 ++++ web/react/components/post.jsx | 88 ++ web/react/components/post_body.jsx | 141 +++ web/react/components/post_deleted_modal.jsx | 36 + web/react/components/post_header.jsx | 23 + web/react/components/post_info.jsx | 52 ++ web/react/components/post_list.jsx | 474 ++++++++++ web/react/components/post_right.jsx | 397 ++++++++ web/react/components/rename_channel_modal.jsx | 142 +++ web/react/components/rename_team_modal.jsx | 92 ++ web/react/components/search_bar.jsx | 104 +++ web/react/components/search_results.jsx | 180 ++++ web/react/components/setting_item_max.jsx | 31 + web/react/components/setting_item_min.jsx | 14 + web/react/components/setting_picture.jsx | 55 ++ web/react/components/settings_modal.jsx | 59 ++ web/react/components/settings_sidebar.jsx | 24 + web/react/components/sidebar.jsx | 449 +++++++++ web/react/components/sidebar_header.jsx | 134 +++ web/react/components/sidebar_right.jsx | 84 ++ web/react/components/sidebar_right_menu.jsx | 76 ++ web/react/components/signup_team.jsx | 78 ++ web/react/components/signup_team_complete.jsx | 644 +++++++++++++ web/react/components/signup_user_complete.jsx | 145 +++ web/react/components/team_members.jsx | 78 ++ web/react/components/textbox.jsx | 290 ++++++ web/react/components/user_profile.jsx | 71 ++ web/react/components/user_settings.jsx | 1151 ++++++++++++++++++++++++ web/react/components/view_image.jsx | 189 ++++ 61 files changed, 8987 insertions(+) create mode 100644 web/react/components/channel_header.jsx create mode 100644 web/react/components/channel_info_modal.jsx create mode 100644 web/react/components/channel_invite_modal.jsx create mode 100644 web/react/components/channel_loader.jsx create mode 100644 web/react/components/channel_members.jsx create mode 100644 web/react/components/channel_notifications.jsx create mode 100644 web/react/components/command_list.jsx create mode 100644 web/react/components/create_comment.jsx create mode 100644 web/react/components/create_post.jsx create mode 100644 web/react/components/delete_channel_modal.jsx create mode 100644 web/react/components/delete_post_modal.jsx create mode 100644 web/react/components/edit_channel_modal.jsx create mode 100644 web/react/components/edit_post_modal.jsx create mode 100644 web/react/components/email_verify.jsx create mode 100644 web/react/components/error_bar.jsx create mode 100644 web/react/components/file_preview.jsx create mode 100644 web/react/components/file_upload.jsx create mode 100644 web/react/components/find_team.jsx create mode 100644 web/react/components/get_link_modal.jsx create mode 100644 web/react/components/invite_member_modal.jsx create mode 100644 web/react/components/login.jsx create mode 100644 web/react/components/member_list.jsx create mode 100644 web/react/components/member_list_item.jsx create mode 100644 web/react/components/member_list_team.jsx create mode 100644 web/react/components/mention.jsx create mode 100644 web/react/components/mention_list.jsx create mode 100644 web/react/components/message_wrapper.jsx create mode 100644 web/react/components/more_channels.jsx create mode 100644 web/react/components/more_direct_channels.jsx create mode 100644 web/react/components/msg_typing.jsx create mode 100644 web/react/components/navbar.jsx create mode 100644 web/react/components/new_channel.jsx create mode 100644 web/react/components/password_reset.jsx create mode 100644 web/react/components/post.jsx create mode 100644 web/react/components/post_body.jsx create mode 100644 web/react/components/post_deleted_modal.jsx create mode 100644 web/react/components/post_header.jsx create mode 100644 web/react/components/post_info.jsx create mode 100644 web/react/components/post_list.jsx create mode 100644 web/react/components/post_right.jsx create mode 100644 web/react/components/rename_channel_modal.jsx create mode 100644 web/react/components/rename_team_modal.jsx create mode 100644 web/react/components/search_bar.jsx create mode 100644 web/react/components/search_results.jsx create mode 100644 web/react/components/setting_item_max.jsx create mode 100644 web/react/components/setting_item_min.jsx create mode 100644 web/react/components/setting_picture.jsx create mode 100644 web/react/components/settings_modal.jsx create mode 100644 web/react/components/settings_sidebar.jsx create mode 100644 web/react/components/sidebar.jsx create mode 100644 web/react/components/sidebar_header.jsx create mode 100644 web/react/components/sidebar_right.jsx create mode 100644 web/react/components/sidebar_right_menu.jsx create mode 100644 web/react/components/signup_team.jsx create mode 100644 web/react/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_user_complete.jsx create mode 100644 web/react/components/team_members.jsx create mode 100644 web/react/components/textbox.jsx create mode 100644 web/react/components/user_profile.jsx create mode 100644 web/react/components/user_settings.jsx create mode 100644 web/react/components/view_image.jsx (limited to 'web/react/components') diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx new file mode 100644 index 000000000..006c168ba --- /dev/null +++ b/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 += "
" + m.username + "
"; + }); + + return ( +
+
+ {count} +
+
+ ); + } +}); + +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 ( +
+ ); + } + + var description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true}); + var popoverContent = React.renderToString(); + 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 = ; + 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 = ; + } else { + channelTitle = ; + } + } + } + + return ( + + + + + { searchForm } + + +
+ { !isDirect ? +
+
+ + {channelTitle} + + + +
+
{description}
+
+ : + {channelTitle} + } +
+ +
+ ); + } +}); + + diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx new file mode 100644 index 000000000..191297ce4 --- /dev/null +++ b/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 ( + + ); + } +}); diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx new file mode 100644 index 000000000..d41453fab --- /dev/null +++ b/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 ? : null; + + var currentMember = ChannelStore.getCurrentMember(); + var isAdmin = false; + if (currentMember) { + isAdmin = currentMember.roles.indexOf("admin") > -1 || UserStore.getCurrentUser().roles.indexOf("admin") > -1; + } + + return ( + + ); + } +}); + + + + diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx new file mode 100644 index 000000000..5252f275c --- /dev/null +++ b/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
; + } +}); diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx new file mode 100644 index 000000000..cfb8ed41c --- /dev/null +++ b/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 ( + + + ); + } +}); diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx new file mode 100644 index 000000000..085536a0a --- /dev/null +++ b/web/react/components/channel_notifications.jsx @@ -0,0 +1,120 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var ChannelStore = require('../stores/channel_store.jsx'); + +module.exports = React.createClass({ + componentDidMount: function() { + var self = this; + $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + var button = e.relatedTarget; + var channel_id = button.dataset.channelid; + + var notifyLevel = ChannelStore.getMember(channel_id).notify_level; + self.setState({ notify_level: notifyLevel, title: button.dataset.title, channel_id: channel_id }); + }); + }, + getInitialState: function() { + return { notify_level: "", title: "", channel_id: "" }; + }, + handleUpdate: function(e) { + var channel_id = this.state.channel_id; + var notify_level = this.state.notify_level; + + var data = {}; + data["channel_id"] = channel_id; + data["user_id"] = UserStore.getCurrentId(); + data["notify_level"] = this.state.notify_level; + + if (!data["notify_level"] || data["notify_level"].length === 0) return; + + client.updateNotifyLevel(data, + function(data) { + var member = ChannelStore.getMember(channel_id); + member.notify_level = notify_level; + ChannelStore.setChannelMember(member); + $(this.refs.modal.getDOMNode()).modal('hide'); + }.bind(this), + function(err) { + this.setState({ server_error: err.message }); + }.bind(this) + ); + }, + handleRadioClick: function(notifyLevel) { + this.setState({ notify_level: notifyLevel }); + this.refs.modal.getDOMNode().focus(); + }, + handleQuietToggle: function() { + if (this.state.notify_level === "quiet") { + this.setState({ notify_level: "none" }); + this.refs.modal.getDOMNode().focus(); + } else { + this.setState({ notify_level: "quiet" }); + this.refs.modal.getDOMNode().focus(); + } + }, + render: function() { + var server_error = this.state.server_error ?
: 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 ( + + + ); + } +}); diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx new file mode 100644 index 000000000..023f5f760 --- /dev/null +++ b/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 (
); + + var suggestions = [] + + for (var i = 0; i < this.state.suggestions.length; i++) { + if (this.state.suggestions[i].suggestion != this.state.cmd) { + suggestions.push( +
+
{ this.state.suggestions[i].suggestion }
+
{ this.state.suggestions[i].description }
+
+ ); + } + } + + return ( +
+ { suggestions } +
+ ); + } +}); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx new file mode 100644 index 000000000..3534c7573 --- /dev/null +++ b/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 ?
: null; + var post_error = this.state.post_error ? : null; + + var preview =
; + if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) { + preview = ( + + ); + } + var limit_previews = "" + if (this.state.previews.length > 5) { + limit_previews =
+ } + if (this.state.previews.length > 20) { + limit_previews =
+ } + + return ( +
+
+
+ + +
+ +
+ + { post_error } + { server_error } + { limit_previews } +
+
+ { preview } +
+ ); + } +}); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx new file mode 100644 index 000000000..191be9bf8 --- /dev/null +++ b/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("
", "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 ?
: null; + var post_error = this.state.post_error ? : null; + + var preview =
; + if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) { + preview = ( + + ); + } + var limit_previews = "" + if (this.state.previews.length > 5) { + limit_previews =
+ } + if (this.state.previews.length > 20) { + limit_previews =
+ } + + return ( +
+
+
+ + +
+
+ { post_error } + { server_error } + { limit_previews } + { preview } + +
+
+
+ ); + } +}); diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx new file mode 100644 index 000000000..a8c690789 --- /dev/null +++ b/web/react/components/delete_channel_modal.jsx @@ -0,0 +1,58 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client =require('../utils/client.jsx'); +var AsyncClient =require('../utils/async_client.jsx'); +var ChannelStore =require('../stores/channel_store.jsx') + +module.exports = React.createClass({ + handleDelete: function(e) { + if (this.state.channel_id.length != 26) return; + + Client.deleteChannel(this.state.channel_id, + function(data) { + AsyncClient.getChannels(true); + window.location.href = '/channels/town-square'; + }.bind(this), + function(err) { + AsyncClient.dispatchError(err, "handleDelete"); + }.bind(this) + ); + }, + componentDidMount: function() { + var self = this; + $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + var button = $(e.relatedTarget); + self.setState({ title: button.attr('data-title'), channel_id: button.attr('data-channelid') }); + }); + }, + getInitialState: function() { + return { title: "", channel_id: "" }; + }, + render: function() { + + var channelType = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'P' ? "private group" : "channel" + + return ( + + ); + } +}); diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx new file mode 100644 index 000000000..c88b548d1 --- /dev/null +++ b/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 ?
: null; + + return ( + + ); + } +}); diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx new file mode 100644 index 000000000..f1f4eca40 --- /dev/null +++ b/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 ( + + ); + } +}); diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx new file mode 100644 index 000000000..24c2d7322 --- /dev/null +++ b/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 ?
: null; + + return ( + + ); + } +}); diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx new file mode 100644 index 000000000..168608274 --- /dev/null +++ b/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 =

Your email has been verified! Click here to log in.

; + } else { + title = config.SiteName + " Email Not Verified"; + body =

Please verify your email address. Check your inbox for an email.

; + resend = + } + + return ( +
+
+
+

{ title }

+
+
+ { body } + { resend } +
+
+
+ ); + } +}); diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx new file mode 100644 index 000000000..f23dc060e --- /dev/null +++ b/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 ( +
+ {message} + × +
+ ); + } else { + return
; + } + } +}); diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx new file mode 100644 index 000000000..99327c22f --- /dev/null +++ b/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( +
+ + +
+ ); + } else { + previews.push( +
+
+ +
+ ); + } + }.bind(this)); + + for (var i = 0; i < this.props.uploadsInProgress; i++) { + previews.push( +
+ +
+ ); + } + + return ( +
+ {previews} +
+ ); + } +}); diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx new file mode 100644 index 000000000..c03a61c63 --- /dev/null +++ b/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 ( + + ); + } +}); diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx new file mode 100644 index 000000000..329592a73 --- /dev/null +++ b/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 ? : null; + + var divStyle = { + "marginTop": "50px", + } + + if (this.state.sent) { + return ( +
+

{"Find Your " + utils.toTitleCase(strings.Team)}

+

{"An email was sent with links to any " + strings.TeamPlural}

+
+ ); + } + + return ( +
+

Find Your Team

+
+

{"An email will be sent to this address with links to any " + strings.TeamPlural}

+
+ +
+ + { email_error } +
+
+ +
+
+ ); + } +}); diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx new file mode 100644 index 000000000..334591ee3 --- /dev/null +++ b/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 ( + + ); + } else { + return
; + } + } +}); diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx new file mode 100644 index 000000000..1d2bbed84 --- /dev/null +++ b/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] ? : null; + var first_name_error = this.state.first_name_errors[index] ? : null; + var last_name_error = this.state.last_name_errors[index] ? : null; + + invite_sections[index] = ( +
+ { i ? +
+ +
+ : ""} +
+ + { email_error } +
+ { config.AllowInviteNames ? +
+ + { first_name_error } +
+ : "" } + { config.AllowInviteNames ? +
+ + { last_name_error } +
+ : "" } +
+ ); + } + + var server_error = this.state.server_error ?
: null; + + return ( + + ); + } else { + return
; + } + } +}); diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx new file mode 100644 index 000000000..8d82a4b62 --- /dev/null +++ b/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 ? : null; + + return ( +
+
+ { config.SiteName } +
+ Enter your {strings.TeamPlural} domain. +
+
+
+
+
+ { server_error } + +
+
+ +
+
+ Don't remember your {strings.TeamPlural} domain? Find it here +
+
+
+
+
+
+
+
+ {"Want to create your own " + strings.Team + "?"} Sign up now +
+
+
+ ); + } +}); + +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 ? : 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 (); + } + + return ( +
+
+ { subDomain } +
+ { utils.getDomainWithOutSub() } +
+
+
+
+
+ { server_error } + +
+
+ +
+
+ +
+
+ +
+ + +
+ {"Want to create your own " + strings.Team + "?"} Sign up now +
+
+
+ ); + } +}); diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx new file mode 100644 index 000000000..a37392f96 --- /dev/null +++ b/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 = No users to add or manage.; + + return ( +
+ {members.map(function(member) { + return ; + }, this)} + {message} +
+ ); + } +}); diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx new file mode 100644 index 000000000..f0bbff8bd --- /dev/null +++ b/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 = Added; + } else if (this.props.handleInvite) { + invite = Add; + } else if (isAdmin && !isMemberAdmin && (member.id != UserStore.getCurrentId())) { + var self = this; + invite = ( +
+ + {member.roles} + + + +
+ ); + } else { + invite =
{member.roles}
; + } + + var email = member.email.length > 0 ? member.email : ""; + + return ( +
+ + {member.username} + {email} + { invite } +
+ ); + } +}); diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx new file mode 100644 index 000000000..3613d97d8 --- /dev/null +++ b/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 ?
: 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 ( +
+ + {user.full_name.trim() ? user.full_name : user.username} + {user.full_name.trim() ? user.username : email} +
+ + {currentRoles} + + + +
+ { server_error } +
+ ); + } +}); + + +module.exports = React.createClass({ + render: function() { + return ( +
+ { + this.props.users.map(function(user) { + return ; + }, this) + } +
+ ); + } +}); diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx new file mode 100644 index 000000000..ba758688b --- /dev/null +++ b/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 ( +
+ + @{this.props.username}{this.props.name} +
+ ); + } +}); diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx new file mode 100644 index 000000000..8b7e25b04 --- /dev/null +++ b/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 (
); + + 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] = ( + + ); + index++; + } + } + var numMentions = Object.keys(mentions).length; + + if (numMentions < 1) return (
); + + 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 ( +
+
+ { mentions } +
+
+ ); + } +}); diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx new file mode 100644 index 000000000..5fc88a61b --- /dev/null +++ b/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 ( +
{inner}
+ ); + } else { + return
+ } + } +}); diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx new file mode 100644 index 000000000..be2a5e93c --- /dev/null +++ b/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 ?
: null; + var outter = this; + + return ( + + + ); + } +}); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx new file mode 100644 index 000000000..2785dc8e0 --- /dev/null +++ b/web/react/components/more_direct_channels.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var ChannelStore = require('../stores/channel_store.jsx'); +var utils = require('../utils/utils.jsx'); + +module.exports = React.createClass({ + componentDidMount: function() { + var self = this; + $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + var button = e.relatedTarget; + self.setState({ channels: $(button).data('channels') }); + }); + }, + getInitialState: function() { + return { channels: [] }; + }, + render: function() { + var self = this; + + var directMessageItems = this.state.channels.map(function(channel) { + var badge = ""; + var titleClass = "" + + if (!channel.fake) { + var active = channel.id === ChannelStore.getCurrentId() ? "active" : ""; + + if (channel.unread) { + badge = {channel.unread}; + badgesActive = true; + titleClass = "unread-title" + } + return ( +
  • {badge}{channel.display_name}
  • + ); + } else { + return ( +
  • {badge}{channel.display_name}
  • + ); + } + }); + + return ( + + + ); + } +}); diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx new file mode 100644 index 000000000..9d3904757 --- /dev/null +++ b/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 ( + { this.state.text } + ); + } +}); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx new file mode 100644 index 000000000..3821c2772 --- /dev/null +++ b/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 (); + } + else { + return ({ this.state.count }); + } + } +}); + +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 ? : null; + + var subDomain = utils.getSubDomain(); + var subDomainClass = "form-control hidden"; + + if (subDomain == "") { + subDomain = UserStore.getLastDomain(); + subDomainClass = "form-control"; + } + + return ( +
    + Find your team +
    + { server_error } + +
    +
    + +
    +
    + +
    + +
    + ); + } +}); + +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(); + 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 = ; + } else { + channelTitle = ; + } + } + } + + if(this.state.channel.description.length == 0){ + popoverContent = React.renderToString(
    No channel description yet.
    Click here to add one.
    ); + } + } + + var loginForm = currentId == null ? : null; + var navbar_collapse_button = currentId != null ? null : + ; + var sidebar_collapse_button = currentId == null ? null : + ; + var right_sidebar_collapse_button= currentId == null ? null : + ; + + + return ( + + ); +} +}); + + diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx new file mode 100644 index 000000000..13fa5b2cc --- /dev/null +++ b/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 ? : null; + var name_error = this.state.name_error ? : null; + var server_error = this.state.server_error ?
    : null; + + return ( + + ); + } +}); diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx new file mode 100644 index 000000000..24566c7b1 --- /dev/null +++ b/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:

    A password reset link has been sent to {email} for your {this.props.teamName} team on {config.SiteName}.com.

    , 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 ?
    {this.state.update_text}{this.state.more_update_text}
    : null; + var error = this.state.error ?
    : null; + + var subDomain = utils.getSubDomain(); + var subDomainClass = "form-control hidden"; + + if (subDomain == "") { + subDomain = UserStore.getLastDomain(); + subDomainClass = "form-control"; + } + + return ( +
    +
    +

    Password Reset

    + { update_text } +
    +

    {"To reset your password, enter the email address you used to sign up for " + this.props.teamName + "."}

    +
    + +
    +
    + +
    + { error } + +
    +
    +
    + ); + } +}); + +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 ?

    : null; + var error = this.state.error ?
    : 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 ( +
    +
    +

    Password Reset

    +
    +

    {"Enter a new password for your " + this.props.teamName + " " + config.SiteName + " account."}

    +
    + +
    +
    + +
    + { error } + + { update_text } +
    +
    +
    + ); + } +}); + +module.exports = React.createClass({ + getInitialState: function() { + return {}; + }, + render: function() { + + if (this.props.isReset === "false") { + return ( + + ); + } else { + return ( + + ); + } + } +}); diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx new file mode 100644 index 000000000..afe978495 --- /dev/null +++ b/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 ?
    : null; + + if(this.props.sameRoot){ + rootUser = "same--root"; + } + else { + rootUser = "other--root"; + } + + var postType = ""; + if(type != "Post"){ + postType = "post--comment"; + } + + return ( +
    +
    + { !this.props.hideProfilePic ? +
    + +
    + : "" } +
    + + + +
    +
    +
    + ); + } +}); diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx new file mode 100644 index 000000000..55fc32c33 --- /dev/null +++ b/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") { + $('').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 = {profile.username}; + } + + var message = parentPost.message; + + comment = ( +

    + Commented on {name}{apostrophe} message: {utils.replaceHtmlEntities(message)} +

    + ); + + 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( +
    +
    +
    + ); + images.push(filenames[i]); + } else { + postFiles.push( +
    + +
    + +
    + ); + } + } + } + + var embed; + if (postFiles.length === 0 && this.state.links) { + embed = utils.getEmbed(this.state.links[0]); + } + + return ( +
    + { comment } +

    {inner}

    + { filenames && filenames.length > 0 ? +
    + { postFiles } +
    + : "" } + { embed } + + { images.length > 0 ? + + : "" } +
    + ); + } +}); diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx new file mode 100644 index 000000000..307120df3 --- /dev/null +++ b/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 ( + + ); + } else { + return
    ; + } + } +}); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx new file mode 100644 index 000000000..129db6d14 --- /dev/null +++ b/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 ( +
      +
    • +
    • + +
    • +
    + ); + } +}); diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx new file mode 100644 index 000000000..cf01747f0 --- /dev/null +++ b/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 = "}} />{this.props.commentCount}; + } + + return ( +
      +
    • +
    • +
      + { isOwner || (this.props.allowReply === "true" && type != "Comment") ? +
      + +
        + { isOwner ?
      • Edit
      • + : "" } + { isOwner ?
      • Delete
      • + : "" } + { this.props.allowReply === "true" ?
      • Reply
      • + : "" } +
      +
      + : "" } +
      + { comments } +
    • +
    + ); + } +}); diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx new file mode 100644 index 000000000..65247b705 --- /dev/null +++ b/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
    ; + } + + var channel = this.state.channel; + + var more_messages =

    Beginning of Channel

    ; + + if (channel != null) { + if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) { + more_messages = Load more messages; + } 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 = ( +
    +
    + +
    +
    + +
    +

    {"This is the start of your direct message history with " + teammate_name + "." }
    {"Direct messages and files shared here are not shown to people outside this area."}

    +
    + ); + } else { + more_messages = ( +
    +

    {"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."}

    +
    + ); + } + } 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 = ( +
    +

    Welcome

    +

    + Welcome to {ui_name}! +

    + {"This is the first channel " + strings.Team + "mates see when they"} +
    + sign up - use it for posting updates everyone needs to know. +

    + To create a new channel or join an existing one, go to +
    + the Left Hand Sidebar under “Channels” and click “More…”. +
    +

    +
    + ); + } else { + var userStyle = { color: UserStore.getCurrentUser().props.theme } + var ui_type = channel.type === 'P' ? "private group" : "channel"; + more_messages = ( +
    +

    Welcome

    +

    + { 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." } +
    + Set a description + Invite others to this {ui_type} +

    +
    + ); + } + } + } + + 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 = ; + + currentPostDay = utils.getDateForUnixTicks(post.create_at); + if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) { + postCtls.push( +
    +
    +
    {currentPostDay.toDateString()}
    +
    + ); + } + + if (post.create_at > last_viewed && !rendered_last_viewed) { + rendered_last_viewed = true; + postCtls.push( +
    +
    +
    +
    New Messages
    +
    + {postCtl} +
    + ); + } else { + postCtls.push(postCtl); + } + previousPostDay = utils.getDateForUnixTicks(post.create_at); + } + + return ( +
    +
    +
    + { more_messages } + { postCtls } +
    +
    +
    + ); + } +}); + + diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx new file mode 100644 index 000000000..43be60afa --- /dev/null +++ b/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 ? {"< "} : ""; + + return ( +
    + {back}Message Details + +
    + ); + } +}); + +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( +
    +
    +
    + ); + images.push(filenames[i]); + } else { + postFiles.push( +
    + +
    + +
    + ); + } + } + } + + return ( +
    +
    + +
    +
    +
      +
    • +
    • +
    • +
      + { isOwner ? +
      + + +
      + : "" } +
      +
    • +
    +
    +

    {message}

    + { filenames.length > 0 ? +
    + { postFiles } +
    + : "" } + { images.length > 0 ? + + : "" } +
    +
    +
    +
    + ); + } +}); + +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( +
    +
    +
    + ); + images.push(filenames[i]); + } else { + postFiles.push( +
    + +
    + +
    + ); + } + } + } + + var message = utils.textToJsx(this.props.post.message); + + return ( +
    +
    + +
    +
    +
      +
    • +
    • +
    • + { isOwner ? +
      + + +
      + : "" } +
    • +
    +
    +

    {message}

    + { filenames.length > 0 ? +
    + { postFiles } +
    + : "" } + { images.length > 0 ? + + : "" } +
    +
    +
    + ); + } +}); + +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 ( +
    + ); + } + + 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 : ; + + return ( +
    +
    {searchForm}
    +
    + +
    + +
    + { posts_array.map(function(cpost) { + return + })} +
    +
    + +
    +
    +
    +
    + ); + } +}); diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx new file mode 100644 index 000000000..b4ccb2937 --- /dev/null +++ b/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 ? : null; + var name_error = this.state.name_error ? : null; + var server_error = this.state.server_error ?
    : null; + + return ( + + ); + } +}); + diff --git a/web/react/components/rename_team_modal.jsx b/web/react/components/rename_team_modal.jsx new file mode 100644 index 000000000..67a150b9d --- /dev/null +++ b/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 ? : null; + var server_error = this.state.server_error ?
    : null; + + return ( + + ); + } +}); + diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx new file mode 100644 index 000000000..cddb738f9 --- /dev/null +++ b/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 ( +
    +
    + +
    + + +
    +
    + ); + } +}); diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx new file mode 100644 index 000000000..51aefd3b8 --- /dev/null +++ b/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 ( +
    + {title} + +
    + ); + } +}); + +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 ( +
    +
    { channelName }
    +
    + +
    +
    +
      +
    • +
    • +
    +
    {message}
    +
    +
    + ); + } +}); + +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 : ; + + if (results == null) { + return ( +
    +
    Search Results
    +
    + ); + } + + if (!results.order || results.order.length == 0) { + return ( +
    +
    {searchForm}
    +
    + +
    +
    No results
    +
    +
    +
    + ); + } + + var self = this; + return ( +
    +
    {searchForm}
    +
    + +
    + {results.order.map(function(id) { + var post = results.posts[id]; + return + })} +
    +
    +
    + ); + } +}); diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx new file mode 100644 index 000000000..03f05b0cf --- /dev/null +++ b/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 ?
    : null; + var server_error = this.props.server_error ?
    : null; + + var inputs = this.props.inputs; + + return ( +
      +
    • {this.props.title}
    • +
    • +
        +
      • + {inputs} +
      • +
      • +
        + { server_error } + { client_error } + Submit + Cancel +
      • +
      +
    • +
    + ); + } +}); diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx new file mode 100644 index 000000000..2209c74d1 --- /dev/null +++ b/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 ( +
      +
    • {this.props.title}
    • +
    • Edit
    • +
    • {this.props.describe}
    • +
    + ); + } +}); diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx new file mode 100644 index 000000000..62c889b7f --- /dev/null +++ b/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 ?
    : null; + var server_error = this.props.server_error ?
    : null; + + var img = null; + if (this.props.picture) { + img = (); + } else { + img = (); + } + + var self = this; + + return ( +
      +
    • {this.props.title}
    • +
    • +
        +
      • + {img} +
      • +
      • + { server_error } + { client_error } + Upload + Save + Cancel +
      • +
      +
    • +
    + ); + } +}); diff --git a/web/react/components/settings_modal.jsx b/web/react/components/settings_modal.jsx new file mode 100644 index 000000000..57a869f93 --- /dev/null +++ b/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 ( + + ); + } +}); + diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx new file mode 100644 index 000000000..34e3c9203 --- /dev/null +++ b/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 ( + + ); + } +}); diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx new file mode 100644 index 000000000..10017c7ee --- /dev/null +++ b/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 ? : null; + + var subDomain = utils.getSubDomain(); + var subDomainClass = "form-control hidden"; + + if (subDomain == "") { + subDomain = UserStore.getLastDomain(); + subDomainClass = "form-control"; + } + + return ( +
    + {"Find your " + strings.Team} +
    + { server_error } + +
    +
    + +
    +
    + +
    + +
    + ); + } +}); + +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("
    ", "g"); + var post = JSON.parse(msg.props.post); + var msg = post.message.replace(repRegex, "\n").split("\n")[0].replace("", "").replace("", ""); + 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 = {channelMember.mention_count}; + badgesActive = true; + titleClass = "unread-title" + } + + return ( +
  • {badge}{channel.display_name}
  • + ); + }); + + 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 = {channelMember.mention_count}; + badgesActive = true; + titleClass = "unread-title" + } + + return ( +
  • {badge}{channel.display_name}
  • + ); + }); + + 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 = {channel.unread}; + badgesActive = true; + titleClass = "unread-title" + } + + return ( +
  • {badge}{channel.display_name}
  • + ); + } else { + return ( +
  • {badge}{channel.display_name}
  • + ); + } + + }); + + 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) { +
  • Loading...
  • + } + + if (privateChannelItems.length == 0) { +
  • Loading...
  • + } + return ( +
    + + + +
    +
      +
    • Channels+

    • + {channelItems} +
    • More...
    • +
    + +
      +
    • Private Groups+

    • + {privateChannelItems} +
    + +
    +
    + ); + } +}); + +var SidebarLoggedOut = React.createClass({ + render: function() { + return ( +
    + + +
    + ); + } +}); + +module.exports = React.createClass({ + render: function() { + var currentId = UserStore.getCurrentId(); + if (currentId != null) { + return ; + } else { + return ; + } + } +}); diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx new file mode 100644 index 000000000..5a872b7a0 --- /dev/null +++ b/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 = ( +
  • + Invite New Member +
  • + ); + + if (this.props.teamType == "O") { + team_link = ( +
  • + Get Team Invite Link +
  • + ); + } + } + + if (isAdmin) { + manage_link = ( +
  • + Manage Team +
  • + ); + rename_link = ( +
  • + Rename +
  • + ); + } + + 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(
  • ); + + teams.push(
  • Switch to { domain }
  • ); + } + } + + return ( + + ); + } +}); + +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 ( +
    + { teamName } + +
    + ); + } +}); + + + diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx new file mode 100644 index 000000000..8334b345b --- /dev/null +++ b/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 ( +
    + ); + } + + $('.inner__wrap').removeClass('.move--right').addClass('move--left'); + $('.sidebar--left').removeClass('move--right'); + $('.sidebar--right').addClass('move--left'); + $('.sidebar--right').prepend(''); + this.resize(); + setTimeout(function(){ + $('.sidebar__overlay').fadeOut("200", function(){ + $(this).remove(); + }); + },500) + + var content = ""; + + if (this.state.search_visible) { + content = ; + } + else if (this.state.post_right_visible) { + content = ; + } + + return ( +
    + { content } +
    + ); + } +}); diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx new file mode 100644 index 000000000..d0c139d1a --- /dev/null +++ b/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 = ( +
  • + Invite New Member +
  • + ); + + if (this.props.teamType == "O") { + team_link = ( +
  • + Get Team Invite Link +
  • + ); + } + } + + if (isAdmin) { + manage_link = ( +
  • + Manage Team +
  • + ); + rename_link = ( +
  • + Rename +
  • + ); + } + + var siteName = config.SiteName != null ? config.SiteName : ""; + var teamName = this.props.teamName ? this.props.teamName : siteName; + + return ( +
    + + +
    + +
    +
    + ); + } +}); diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx new file mode 100644 index 000000000..22086250c --- /dev/null +++ b/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 ? : null; + var name_error = this.state.name_error ? : null; + var server_error = this.state.server_error ?
    : null; + + return ( +
    +
    + + { email_error } +
    +
    + + { name_error } +
    + { server_error } + +
    + ); + } +}); + + diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx new file mode 100644 index 000000000..066161a10 --- /dev/null +++ b/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 ? : null; + var server_error = this.state.server_error ?
    : null; + + return ( +
    +

    + +

    Welcome!

    +

    {"Let's set up your " + strings.Team + " on " + config.SiteName + "."}

    +

    +

    + Please confirm your email address:
    + { this.props.state.team.email }
    +

    +
    + +
    +
    +

    If this is not correct, you can switch to a different email. We'll send you a new invite right away.

    +
    +
    +
    +
    + +
    +
    + { email_error } +
    + { server_error } + +
    + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + + +

    {utils.toTitleCase(strings.Team) + " Name"}

    +
    +
    +
    + +
    +
    + { name_error } +
    +

    {"Your " + strings.Team + " name shows in menus and headings. It may include the name of your " + strings.Company + ", but it's not required."}

    +   + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + +

    {utils.toTitleCase(strings.Team) + " URL"}

    +
    +
    +
    +
    + + .{ utils.getDomainWithOutSub() } +
    +
    +
    + { name_error } +
    +

    {"Pick something short and memorable for your " + strings.Team + "'s web address."}

    +

    {"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."}

    +   + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + +

    Email Domain

    +

    +

    +

    +

    {"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."}

    +

    {"Your " + strings.Team + "'s domain for emails"}

    +
    +
    +
    +
    + @ + +
    +
    +
    + { name_error } +
    +

    To allow signups from multiple domains, separate each with a comma.

    +

    +

    +

    +   + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + + { email_error } +
    + ); + } +}); + + +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 ? : null; + + var emails = []; + + for (var i = 0; i < this.props.state.invites.length; i++) { + emails.push(); + } + + return ( +
    + +

    Send Invitations

    + { emails } +
    +
     
    +

    {"If you'd prefer, you can send invitations after you finish setting up the "+ strings.Team + "."}

    + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + +

    Choose a username

    +
    +
    +
    + +
    +
    + { name_error } +
    +

    {"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}

    +   + +
    + ); + } +}); + +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 ? : null; + + return ( +
    + +

    Choose a password

    +

    You'll use your email address ({this.props.state.team.email}) and password to log into {config.SiteName}.

    +
    +
    +
    + +
    +
    + { name_error } +
    +
    + +
    +
    +   + +
    +

    By proceeding to create your account and use { config.SiteName }, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {config.SiteName}.

    +
    + ); + } +}); + +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 + } + + if (this.state.wizard == "team_name") { + return + } + + if (this.state.wizard == "team_url") { + return + } + + if (this.state.wizard == "allowed_domains") { + return + } + + if (this.state.wizard == "send_invites") { + return + } + + if (this.state.wizard == "username") { + return + } + + if (this.state.wizard == "password") { + return + } + + return (
    You've already completed the signup process for this invitation or this invitation has expired.
    ); + } +}); + + diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx new file mode 100644 index 000000000..0fcdc92b0 --- /dev/null +++ b/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 (
    You've already completed the signup process for this invitation or this invitation has expired.
    ); + } + + var email_error = this.state.email_error ? : null; + var name_error = this.state.name_error ? : null; + var password_error = this.state.password_error ? : null; + var server_error = this.state.server_error ?
    : null; + + var yourEmailIs = this.state.user.email == "" ? "" : Your email address is { this.state.user.email }. + + var email = +
    + +
    + + { email_error } +
    +
    + + return ( +
    + +

    Welcome to { config.SiteName }

    +

    {"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}

    + +
    + + { name_error } +
    + { email } + +
    + + { password_error } +
    +

    {"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}

    +

    { yourEmailIs } You’ll use this address to sign in to {config.SiteName}.

    +
    +

    + { server_error } +

    By proceeding to create your account and use { config.SiteName }, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {config.SiteName}.

    +
    + ); + } +}); + + diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx new file mode 100644 index 000000000..6b978f88b --- /dev/null +++ b/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 ? : null; + + return ( + + ); + } +}); diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx new file mode 100644 index 000000000..45798809f --- /dev/null +++ b/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@'+m+'$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 ( +
    + +
    +